From c22443acc751d073a64e8c6640135e6a66551e51 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 31 Jul 2021 21:06:28 -0400 Subject: [PATCH 01/95] AutoRecon v2 Beta Release Effectively an initial commit since this is 95% rewritten code. --- .gitignore | 141 +- Dockerfile | 16 - README.md | 424 +---- autorecon.py | 1429 +++++++++++++++++ config.toml | 20 + global.toml | 7 + {src/autorecon => plugins}/__init__.py | 0 plugins/databases.py | 122 ++ plugins/default-port-scan.py | 46 + plugins/dns.py | 14 + plugins/ftp.py | 30 + plugins/http.py | 186 +++ plugins/kerberos.py | 14 + plugins/ldap.py | 29 + plugins/misc.py | 170 ++ plugins/nfs.py | 27 + plugins/rdp.py | 30 + plugins/rpc.py | 27 + plugins/sip.py | 27 + plugins/smb.py | 85 + plugins/snmp.py | 52 + plugins/ssh.py | 30 + plugins/sslscan.py | 16 + poetry.lock | 42 - poetry.toml | 2 - pyproject.toml | 28 - requirements.txt | 4 +- src/autorecon/autorecon.py | 885 ---------- .../config/global-patterns-default.toml | 8 - .../config/port-scan-profiles-default.toml | 45 - .../config/service-scans-default.toml | 586 ------- 31 files changed, 2370 insertions(+), 2172 deletions(-) delete mode 100644 Dockerfile create mode 100644 autorecon.py create mode 100644 config.toml create mode 100644 global.toml rename {src/autorecon => plugins}/__init__.py (100%) create mode 100644 plugins/databases.py create mode 100644 plugins/default-port-scan.py create mode 100644 plugins/dns.py create mode 100644 plugins/ftp.py create mode 100644 plugins/http.py create mode 100644 plugins/kerberos.py create mode 100644 plugins/ldap.py create mode 100644 plugins/misc.py create mode 100644 plugins/nfs.py create mode 100644 plugins/rdp.py create mode 100644 plugins/rpc.py create mode 100644 plugins/sip.py create mode 100644 plugins/smb.py create mode 100644 plugins/snmp.py create mode 100644 plugins/ssh.py create mode 100644 plugins/sslscan.py delete mode 100644 poetry.lock delete mode 100644 poetry.toml delete mode 100644 pyproject.toml delete mode 100755 src/autorecon/autorecon.py delete mode 100644 src/autorecon/config/global-patterns-default.toml delete mode 100644 src/autorecon/config/port-scan-profiles-default.toml delete mode 100644 src/autorecon/config/service-scans-default.toml diff --git a/.gitignore b/.gitignore index e1811a8..463d071 100644 --- a/.gitignore +++ b/.gitignore @@ -1,139 +1,4 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +__pycache__ +plugins/__pycache__ *.pyc - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ +results/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cae11fb..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -#sudo docker build -t tib3rius/autorecon . -#mkdir /root/results -#Usage: sudo docker run -it -v /root/results:/results --rm --name autorecon-container tib3rius/autorecon 127.0.0.1 - -FROM kalilinux/kali-bleeding-edge:latest -RUN apt-get update -RUN apt-get install sudo git -y -RUN sudo apt install python3 -y -RUN sudo apt install python3-pip -y -RUN python3 -m pip install --user pipx -RUN python3 -m pipx ensurepath -RUN python3 -m pip install git+https://github.com/Tib3rius/AutoRecon.git -RUN sudo apt install seclists curl enum4linux gobuster nbtscan nikto nmap onesixtyone oscanner smbclient smbmap smtp-user-enum snmp sslscan sipvicious tnscmd10g whatweb wkhtmltopdf -y -RUN echo "Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/kali/.local/bin"" >> /etc/sudoers -RUN apt-get -y autoremove && apt-get -y autoclean -ENTRYPOINT ["autorecon"] diff --git a/README.md b/README.md index f2bcf53..b67a694 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[![Packaging status](https://repology.org/badge/vertical-allrepos/autorecon.svg)](https://repology.org/project/autorecon/versions) - > It's like bowling with bumpers. - [@ippsec](https://twitter.com/ippsec) # AutoRecon @@ -24,77 +22,11 @@ AutoRecon was inspired by three tools which the author used during the OSCP labs * Full logging of commands that were run, along with errors if they fail. * Global and per-scan pattern matching so you can highlight/extract important information from the noise. -## Quick Install (Docker) - -If you don't have a Kali instance, you can quickly install AutoRecon using the Dockerfile in the repository. Simply download the Dockerfile, and run the following command from the same directory: - -```bash -sudo docker build -t tib3rius/autorecon . -``` - ## Requirements - Python 3 - `python3-pip` -- `pipx` (optional, but recommended) -### Python 3 - -If you don't have these installed, and are running Kali Linux, you can execute the following: - -```bash -$ sudo apt install python3 -$ sudo apt install python3-pip -``` - -Additionally, if you experience any issues with the stability of the `python3-pip` installation (as reported by a number of people installing `pip3` via `apt` on the OSCP distribution of Kali), you can install it manually as follows: - -```bash -$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py -$ python3 get-pip.py -``` - -The `pip3` command should now be usable. - -### `pipx` - -Further, it's recommended you use `pipx` to manage your python packages; this installs each python package in it's own virtualenv, and makes it available in the global context, which avoids conflicting package dependencies and the resulting instability. To summarise the installation instructions: - -```bash -$ sudo apt install python3-venv -$ python3 -m pip install --user pipx -$ python3 -m pipx ensurepath -``` - -You will have to re-source your ~/.bashrc or ~/.zshrc file (or open a new tab) after running these commands in order to use pipx. - -Note that if you want to elevate privileges to run a program installed with `pipx`, with `sudo`, you have two options: - -1. Append the appropriate path to your execution command, using _one_ of the following examples (recommended): - -```bash -$ sudo env "PATH=$PATH" autorecon [OPTIONS] -$ sudo $(which autorecon) [OPTIONS] -``` - -To make this easier, you could add the following alias to your `~/.profile` (or equivalent): - -``` -alias sudo="sudo env \"PATH=$PATH\"" -``` - -2. Add the `pipx` binary path to the `secure_path` set in `/etc/sudoers` - -```bash -sudo visudo /etc/sudoers -``` - -Update the `secure_path` directive as follows: -``` -Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/kali/.local/bin" -``` - -If you're not using Kali Linux, make sure to adjust the path to the relevant user. Further detail on the installation of `pipx` is available in their installation instructions available [here](https://pipxproject.github.io/pipx/installation/). Please refer to this for any issues you experience. ### Supporting packages @@ -125,7 +57,7 @@ sslscan svwar tnscmd10g whatweb -wkhtmltoimage +wkhtmltopdf ``` On Kali Linux, you can ensure these are all installed using the following command: @@ -138,29 +70,13 @@ $ sudo apt install seclists curl enum4linux feroxbuster nbtscan nikto nmap onesi Ensure you have all of the requirements installed as per the previous section. -### Using `pipx` (recommended) - -```bash -$ pipx install git+https://github.com/Tib3rius/AutoRecon.git -``` - -### Using `pip` - -If installing using pip, you'll need to run the installation command as root or with sudo in order to be able to run autorecon using sudo: - -```bash -$ sudo python3 -m pip install git+https://github.com/Tib3rius/AutoRecon.git -``` - -### Manual - -If you'd prefer not to use `pip` or `pipx`, you can always still install and execute `autorecon.py` manually as a script. First install the dependencies: +First install the dependencies: ```bash $ python3 -m pip install -r requirements.txt ``` -You will then be able to run the `autorecon.py` script (from `/src/autorecon`): +You will then be able to run the `autorecon.py` script: ```bash $ python3 autorecon.py [OPTIONS] 127.0.0.1 @@ -168,176 +84,9 @@ $ python3 autorecon.py [OPTIONS] 127.0.0.1 See detailed usage options below. -### Upgrading - -If you installed using pipx or pip it is recommended that prior to upgrading, you remove all config files stored in ~/.config/AutoRecon. Note that if you run AutoRecon using sudo, you'll also have to delete the config files in /root/.config/AutoRecon. - ## Usage -AutoRecon uses Python 3 specific functionality and does not support Python 2. - -``` -usage: autorecon [-h] [-t TARGET_FILE] [-ct ] [-cs ] - [--profile PROFILE_NAME] [-o OUTPUT_DIR] [--single-target] - [--only-scans-dir] [--heartbeat HEARTBEAT] - [--nmap NMAP | --nmap-append NMAP_APPEND] [-v] - [--disable-sanity-checks] - [targets [targets ...]] - -Network reconnaissance tool to port scan and automatically enumerate services -found on multiple targets. - -positional arguments: - targets IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. - 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) - to scan. - -optional arguments: - -h, --help show this help message and exit - -t TARGET_FILE, --targets TARGET_FILE - Read targets from file. - -ct , --concurrent-targets - The maximum number of target hosts to scan - concurrently. Default: 5 - -cs , --concurrent-scans - The maximum number of scans to perform per target - host. Default: 10 - --profile PROFILE_NAME - The port scanning profile to use (defined in port- - scan-profiles.toml). Default: default - -o OUTPUT_DIR, --output OUTPUT_DIR - The output directory for results. Default: results - --single-target Only scan a single target. A directory named after the - target will not be created. Instead, the directory - structure will be created within the output directory. - Default: false - --only-scans-dir Only create the "scans" directory for results. Other - directories (e.g. exploit, loot, report) will not be - created. Default: false - --heartbeat HEARTBEAT - Specifies the heartbeat interval (in seconds) for task - status messages. Default: 60 - --nmap NMAP Override the {nmap_extra} variable in scans. Default: - -vv --reason -Pn - --nmap-append NMAP_APPEND - Append to the default {nmap_extra} variable in scans. - -v, --verbose Enable verbose output. Repeat for more verbosity. - --disable-sanity-checks - Disable sanity checks that would otherwise prevent the - scans from running. Default: false -``` - -### Examples - -**Scanning a single target:** - -``` -$ autorecon 127.0.0.1 -[*] Scanning target 127.0.0.1 -[*] Running service detection nmap-full-tcp on 127.0.0.1 -[*] Running service detection nmap-top-20-udp on 127.0.0.1 -[*] Running service detection nmap-quick on 127.0.0.1 -[*] Service detection nmap-quick on 127.0.0.1 finished successfully -[*] [127.0.0.1] ssh found on tcp/22 -[*] [127.0.0.1] http found on tcp/80 -[*] [127.0.0.1] rpcbind found on tcp/111 -[*] [127.0.0.1] postgresql found on tcp/5432 -[*] Running task tcp/22/nmap-ssh on 127.0.0.1 -[*] Running task tcp/80/nmap-http on 127.0.0.1 -[*] Running task tcp/80/curl-index on 127.0.0.1 -[*] Running task tcp/80/curl-robots on 127.0.0.1 -[*] Running task tcp/80/whatweb on 127.0.0.1 -[*] Running task tcp/80/nikto on 127.0.0.1 -[*] Running task tcp/111/nmap-nfs on 127.0.0.1 -[*] Task tcp/80/curl-index on 127.0.0.1 finished successfully -[*] Task tcp/80/curl-robots on 127.0.0.1 finished successfully -[*] Task tcp/22/nmap-ssh on 127.0.0.1 finished successfully -[*] Task tcp/80/whatweb on 127.0.0.1 finished successfully -[*] Task tcp/111/nmap-nfs on 127.0.0.1 finished successfully -[*] Task tcp/80/nmap-http on 127.0.0.1 finished successfully -[*] Task tcp/80/nikto on 127.0.0.1 finished successfully -[*] Service detection nmap-top-20-udp on 127.0.0.1 finished successfully -[*] Service detection nmap-full-tcp on 127.0.0.1 finished successfully -[*] [127.0.0.1] http found on tcp/5984 -[*] [127.0.0.1] rtsp found on tcp/5985 -[*] Running task tcp/5984/nmap-http on 127.0.0.1 -[*] Running task tcp/5984/curl-index on 127.0.0.1 -[*] Running task tcp/5984/curl-robots on 127.0.0.1 -[*] Running task tcp/5984/whatweb on 127.0.0.1 -[*] Running task tcp/5984/nikto on 127.0.0.1 -[*] Task tcp/5984/curl-index on 127.0.0.1 finished successfully -[*] Task tcp/5984/curl-robots on 127.0.0.1 finished successfully -[*] Task tcp/5984/whatweb on 127.0.0.1 finished successfully -[*] Task tcp/5984/nikto on 127.0.0.1 finished successfully -[*] Task tcp/5984/nmap-http on 127.0.0.1 finished successfully -[*] Finished scanning target 127.0.0.1 -``` - -The default port scan profile first performs a full TCP port scan, a top 20 UDP port scan, and a top 1000 TCP port scan. You may ask why AutoRecon scans the top 1000 TCP ports at the same time as a full TCP port scan (which also scans those ports). The reason is simple: most open ports will generally be in the top 1000, and we want to start enumerating services quickly, rather than wait for Nmap to scan every single port. As you can see, all the service enumeration scans actually finish before the full TCP port scan is done. While there is a slight duplication of efforts, it pays off by getting actual enumeration results back to the tester quicker. - -Note that the actual command line output will be colorized if your terminal supports it. - -**Scanning multiple targets** - -``` -$ autorecon 192.168.1.100 192.168.1.1/30 localhost -[*] Scanning target 192.168.1.100 -[*] Scanning target 192.168.1.1 -[*] Scanning target 192.168.1.2 -[*] Scanning target localhost -[*] Running service detection nmap-quick on 192.168.1.100 -[*] Running service detection nmap-quick on localhost -[*] Running service detection nmap-top-20-udp on 192.168.1.100 -[*] Running service detection nmap-quick on 192.168.1.1 -[*] Running service detection nmap-quick on 192.168.1.2 -[*] Running service detection nmap-top-20-udp on 192.168.1.1 -[*] Running service detection nmap-full-tcp on 192.168.1.100 -[*] Running service detection nmap-top-20-udp on localhost -[*] Running service detection nmap-top-20-udp on 192.168.1.2 -[*] Running service detection nmap-full-tcp on localhost -[*] Running service detection nmap-full-tcp on 192.168.1.1 -[*] Running service detection nmap-full-tcp on 192.168.1.2 -... -``` - -AutoRecon supports multiple targets per scan, and will expand IP ranges provided in CIDR notation. By default, only 5 targets will be scanned at a time, with 10 scans per target. - -**Scanning multiple targets with advanced options** - -``` -$ autorecon -ct 2 -cs 2 -vv -o outputdir 192.168.1.100 192.168.1.1/30 localhost -[*] Scanning target 192.168.1.100 -[*] Scanning target 192.168.1.1 -[*] Running service detection nmap-quick on 192.168.1.100 with nmap -vv --reason -Pn -sV -sC --version-all -oN "/root/outputdir/192.168.1.100/scans/_quick_tcp_nmap.txt" -oX "/root/outputdir/192.168.1.100/scans/_quick_tcp_nmap.xml" 192.168.1.100 -[*] Running service detection nmap-quick on 192.168.1.1 with nmap -vv --reason -Pn -sV -sC --version-all -oN "/root/outputdir/192.168.1.1/scans/_quick_tcp_nmap.txt" -oX "/root/outputdir/192.168.1.1/scans/_quick_tcp_nmap.xml" 192.168.1.1 -[*] Running service detection nmap-top-20-udp on 192.168.1.100 with nmap -vv --reason -Pn -sU -A --top-ports=20 --version-all -oN "/root/outputdir/192.168.1.100/scans/_top_20_udp_nmap.txt" -oX "/root/outputdir/192.168.1.100/scans/_top_20_udp_nmap.xml" 192.168.1.100 -[*] Running service detection nmap-top-20-udp on 192.168.1.1 with nmap -vv --reason -Pn -sU -A --top-ports=20 --version-all -oN "/root/outputdir/192.168.1.1/scans/_top_20_udp_nmap.txt" -oX "/root/outputdir/192.168.1.1/scans/_top_20_udp_nmap.xml" 192.168.1.1 -[-] [192.168.1.1 nmap-quick] Starting Nmap 7.70 ( https://nmap.org ) at 2019-03-01 17:25 EST -[-] [192.168.1.100 nmap-quick] Starting Nmap 7.70 ( https://nmap.org ) at 2019-03-01 17:25 EST -[-] [192.168.1.100 nmap-top-20-udp] Starting Nmap 7.70 ( https://nmap.org ) at 2019-03-01 17:25 EST -[-] [192.168.1.1 nmap-top-20-udp] Starting Nmap 7.70 ( https://nmap.org ) at 2019-03-01 17:25 EST -[-] [192.168.1.1 nmap-quick] NSE: Loaded 148 scripts for scanning. -[-] [192.168.1.1 nmap-quick] NSE: Script Pre-scanning. -[-] [192.168.1.1 nmap-quick] NSE: Starting runlevel 1 (of 2) scan. -[-] [192.168.1.1 nmap-quick] Initiating NSE at 17:25 -[-] [192.168.1.1 nmap-quick] Completed NSE at 17:25, 0.00s elapsed -[-] [192.168.1.1 nmap-quick] NSE: Starting runlevel 2 (of 2) scan. -[-] [192.168.1.1 nmap-quick] Initiating NSE at 17:25 -[-] [192.168.1.1 nmap-quick] Completed NSE at 17:25, 0.00s elapsed -[-] [192.168.1.1 nmap-quick] Initiating ARP Ping Scan at 17:25 -[-] [192.168.1.100 nmap-quick] NSE: Loaded 148 scripts for scanning. -[-] [192.168.1.100 nmap-quick] NSE: Script Pre-scanning. -[-] [192.168.1.100 nmap-quick] NSE: Starting runlevel 1 (of 2) scan. -[-] [192.168.1.100 nmap-quick] Initiating NSE at 17:25 -[-] [192.168.1.100 nmap-quick] Completed NSE at 17:25, 0.00s elapsed -[-] [192.168.1.100 nmap-quick] NSE: Starting runlevel 2 (of 2) scan. -[-] [192.168.1.100 nmap-quick] Initiating NSE at 17:25 -[-] [192.168.1.100 nmap-quick] Completed NSE at 17:25, 0.00s elapsed -[-] [192.168.1.100 nmap-quick] Initiating ARP Ping Scan at 17:25 -... -``` - -In this example, the -ct option limits the number of concurrent targets to 2, and the -cs option limits the number of concurrent scans per target to 2. The -vv option makes the output very verbose, showing the output of every scan being run. The -o option sets a custom output directory for scan results to be saved. +TODO ### Verbosity @@ -386,171 +135,6 @@ If output matches a defined pattern, a file called \_patterns.log will also appe The scans/xml directory stores any XML output (e.g. from Nmap scans) separately from the main scan outputs, so that the scans directory itself does not get too cluttered. -### Port Scan profiles - -The port-scan-profiles.toml file is where you can define the initial port scans / service detection commands. The configuration file uses the TOML format, which is explained here: https://github.com/toml-lang/toml - -Here is an example profile called "quick": - -```toml -[quick] - - [quick.nmap-quick] - - [quick.nmap-quick.service-detection] - command = 'nmap {nmap_extra} -sV --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - - [quick.nmap-top-20-udp] - - [quick.nmap-top-20-udp.service-detection] - command = 'nmap {nmap_extra} -sU -A --top-ports=20 --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' -``` - -Note that indentation is optional, it is used here purely for aesthetics. The "quick" profile defines a scan called "nmap-quick". This scan has a service-detection command which uses nmap to scan the top 1000 TCP ports. The command uses two references: {scandir} is the location of the scans directory for the target, and {address} is the address of the target. - -A regex pattern is defined which matches three named groups (port, protocol, and service) in the output. Every service-detection command must have a corresponding pattern that matches all three of those groups. AutoRecon will attempt to do some checks and refuse to scan if any of these groups are missing. - -An almost identical scan called "nmap-top-20-udp" is also defined. This scans the top 20 UDP ports. - -Here is a more complicated example: - -```toml -[udp] - - [udp.udp-top-20] - - [udp.udp-top-20.port-scan] - command = 'unicornscan -mU -p 631,161,137,123,138,1434,445,135,67,53,139,500,68,520,1900,4500,514,49152,162,69 {address} 2>&1 | tee "{scandir}/_top_20_udp_unicornscan.txt"' - pattern = '^UDP open\s*[\w-]+\[\s*(?P\d+)\].*$' - - [udp.udp-top-20.service-detection] - command = 'nmap {nmap_extra} -sU -A -p {ports} --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' -``` - -In this example, a profile called "udp" defines a scan called "udp-top-20". This scan has two commands, one is a port-scan and the other is a service-detection. When a port-scan command is defined, it will always be run first. The corresponding pattern must match a named group "port" which extracts the port number from the output. - -The service-detection will be run after the port-scan command has finished, and uses a new reference: {ports}. This reference is a comma-separated string of all the ports extracted by the port-scan command. Note that the same three named groups (port, protocol, and service) are defined in the service-detection pattern. - -Both the port-scan and the service-detection commands use the {scandir} and {address} references. - -Note that if a port-scan command is defined without a corresponding service-detection command, AutoRecon will refuse to scan. - -This more complicated example is only really useful if you want to use unicornscan's speed in conjuction with nmap's service detection abilities. If you are content with using Nmap for both port scanning and service detection, you do not need to use this setup. - -### Service Scans - -The service-scans.toml file is where you can define service enumeration scans and other manual commands associated with certain services. - -Here is an example of a simple configuration: - -```toml -[ftp] - -service-names = [ - '^ftp', - '^ftp\-data' -] - - [[ftp.scan]] - name = 'nmap-ftp' - command = 'nmap {nmap_extra} -sV -p {port} --script="(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}' - - [[ftp.scan.pattern]] - description = 'Anonymous FTP Enabled!' - pattern = 'Anonymous FTP login allowed' - - [[ftp.manual]] - description = 'Bruteforce logins:' - commands = [ - 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', - 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' - ] -``` - -Note that indentation is optional, it is used here purely for aesthetics. The service "ftp" is defined here. The service-names array contains regex strings which should match the service name from the service-detection scans. Regex is used to be as flexible as possible. The service-names array works on a whitelist basis; as long as one of the regex strings matches, the service will get scanned. - -An optional ignore-service-names array can also be defined, if you want to blacklist certain regex strings from matching. - -The ftp.scan section defines a single scan, named nmap-ftp. This scan defines a command which runs nmap with several ftp-related scripts. Several references are used here: -* {nmap_extra} by default is set to "-vv --reason -Pn" but this can be overridden or appended to using the --nmap or --nmap-append command line options respectively. If the protocol is UDP, "-sU" will also be appended. -* {port} is the port that the service is running on. -* {scandir} is the location of the scans directory for the target. -* {protocol} is the protocol being used (either tcp or udp). -* {address} is the address of the target. - -A pattern is defined for the nmap-ftp scan, which matches the simple pattern "Anonymous FTP login allowed". In the event that this pattern matches output of the nmap-ftp command, the pattern description ("Anonymous FTP Enabled!") will be saved to the \_patterns.log file in the scans directory. A special reference {match} can be used in the description to reference the entire match, or the first capturing group. - -The ftp.manual section defines a group of manual commands. This group contains a description for the user, and a commands array which contains the commands that a user can run. Two new references are defined here: {username_wordlist} and {password_wordlist} which are configured at the very top of the service-scans.toml file, and default to a username and password wordlist provided by SecLists. - -Here is a more complicated configuration: - -```toml -[smb] - -service-names = [ - '^smb', - '^microsoft\-ds', - '^netbios' -] - - [[smb.scan]] - name = 'nmap-smb' - command = 'nmap {nmap_extra} -sV -p {port} --script="(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}' - - [[smb.scan]] - name = 'enum4linux' - command = 'enum4linux -a -M -l -d {address} 2>&1 | tee "{scandir}/enum4linux.txt"' - run_once = true - ports.tcp = [139, 389, 445] - ports.udp = [137] - - [[smb.scan]] - name = 'nbtscan' - command = 'nbtscan -rvh {address} 2>&1 | tee "{scandir}/nbtscan.txt"' - run_once = true - ports.udp = [137] - - [[smb.scan]] - name = 'smbclient' - command = 'smbclient -L\\ -N -I {address} 2>&1 | tee "{scandir}/smbclient.txt"' - run_once = true - ports.tcp = [139, 445] - - [[smb.scan]] - name = 'smbmap-share-permissions' - command = 'smbmap -H {address} -P {port} 2>&1 | tee -a "{scandir}/smbmap-share-permissions.txt"; smbmap -u null -p "" -H {address} -P {port} 2>&1 | tee -a "{scandir}/smbmap-share-permissions.txt"' - - [[smb.scan]] - name = 'smbmap-list-contents' - command = 'smbmap -H {address} -P {port} -R 2>&1 | tee -a "{scandir}/smbmap-list-contents.txt"; smbmap -u null -p "" -H {address} -P {port} -R 2>&1 | tee -a "{scandir}/smbmap-list-contents.txt"' - - [[smb.scan]] - name = 'smbmap-execute-command' - command = 'smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1 | tee -a "{scandir}/smbmap-execute-command.txt"; smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1 | tee -a "{scandir}/smbmap-execute-command.txt"' - - [[smb.manual]] - description = 'Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:' - commands = [ - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' - ] -``` - -The main difference here is that several scans have some new settings: - -* The ports.tcp array defines a whitelist of TCP ports which the command can be run against. If the service is detected on a port that is not in the whitelist, the command will not be run against it. -* The ports.udp array defines a whitelist of UDP ports which the command can be run against. It operates in the same way as the ports.tcp array. - -Why do these settings even exist? Well, some commands will only run against specific ports, and can't be told to run against any other ports. enum4linux for example, will only run against TCP ports 139, 389, and 445, and UDP port 137. - -In fact, enum4linux will always try these ports when it is run. So if the SMB service is found on TCP ports 139 and 445, AutoRecon may attempt to run enum4linux twice for no reason. This is why the third setting exists: - -* If run_once is set to true, the command will only ever run once for that target, even if the SMB service is found on multiple ports. - ## Testimonials > AutoRecon was invaluable during my OSCP exam, in that it saved me from the tedium of executing my active information gathering commands myself. I was able to start on a target with all of the information I needed clearly laid in front of me. I would strongly recommend this utility for anyone in the PWK labs, the OSCP exam, or other environments such as VulnHub or HTB. It is a great tool for both people just starting down their journey into OffSec and seasoned veterans alike. Just make sure that somewhere between those two points you take the time to learn what's going on "under the hood" and how / why it scans what it does. diff --git a/autorecon.py b/autorecon.py new file mode 100644 index 0000000..6993359 --- /dev/null +++ b/autorecon.py @@ -0,0 +1,1429 @@ +import asyncio, os, sys, re, signal, pkgutil, inspect, importlib, unidecode, argparse, string, ipaddress, socket, toml, time, math +from datetime import datetime +import colorama +from typing import final +from colorama import Fore, Style +import traceback + +colorama.init() + +class Pattern: + + def __init__(self, pattern, description=None): + self.pattern = pattern + self.description = description + +class Target: + + def __init__(self, address, autorecon): + self.address = address + self.autorecon = autorecon + self.basedir = '' + self.reportdir = '' + self.scandir = '' + self.lock = asyncio.Lock() + self.pending_services = [] + self.services = [] + self.scans = [] + self.running_tasks = {} + + async def add_service(self, protocol, port, name, secure=False): + async with self.lock: + self.pending_services.append(Service(protocol, port, name, secure)) + + def extract_service(self, line, regex=None): + return self.autorecon.extract_service(line, regex) + + async def extract_services(self, stream, regex=None): + return await self.autorecon.extract_services(stream, regex) + + async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + target = self + + # Create variables for command references. + address = target.address + scandir = target.scandir + + nmap_extra = self.autorecon.args.nmap + if self.autorecon.args.nmap_append: + nmap_extra += ' ' + self.autorecon.args.nmap_append + + plugin = inspect.currentframe().f_back.f_locals['self'] + + cmd = e(cmd) + + tag = plugin.slug + + if target.autorecon.config['verbose'] >= 1: + info('Port scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + + if outfile is not None: + outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) + + if errfile is not None: + errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) + + async with target.lock: + with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: + file.writelines(cmd + '\n\n') + + process, stdout, stderr = await self.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) + + target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) + + if blocking: + while (not (stdout.ended and stderr.ended)): + await asyncio.sleep(0.1) + await process.wait() + + return process, stdout, stderr + +class Service: + + def __init__(self, protocol, port, name, secure=False): + self.target = None + self.protocol = protocol.lower() + self.port = int(port) + self.name = name + self.secure = secure + + @final + def tag(self): + return self.protocol + '/' + str(self.port) + '/' + self.name + + @final + def full_tag(self): + return self.protocol + '/' + str(self.port) + '/' + self.name + '/' + ('secure' if self.secure else 'insecure') + + @final + async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + target = self.target + + # Create variables for command references. + address = target.address + scandir = target.scandir + protocol = self.protocol + port = self.port + name = self.name + + # Special cases for HTTP. + http_scheme = 'https' if 'https' in self.name or self.secure is True else 'http' + + nmap_extra = self.target.autorecon.args.nmap + if self.target.autorecon.args.nmap_append: + nmap_extra += ' ' + self.target.autorecon.args.nmap_append + + if protocol == 'udp': + nmap_extra += ' -sU' + + plugin = inspect.currentframe().f_back.f_locals['self'] + + cmd = e(cmd) + + tag = self.tag() + '/' + plugin.slug + + if target.autorecon.config['verbose'] >= 1: + info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + #info('{blue}[{bright}' + address + ' ' + tag + '{srst}]{rst} Running command: ' + cmd) + + if outfile is not None: + outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) + + if errfile is not None: + errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) + + async with target.lock: + with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: + file.writelines(e('{cmd}\n\n')) + + process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) + + target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) + + if blocking: + while (not (stdout.ended and stderr.ended)): + await asyncio.sleep(0.1) + await process.wait() + + return process, stdout, stderr + +class CommandStreamReader(object): + + def __init__(self, stream, target, tag,patterns=None, outfile=None): + self.stream = stream + self.target = target + self.tag = tag + self.lines = [] + self.patterns = patterns or [] + self.outfile = outfile + self.ended = False + + async def _read(self): + while True: + if self.stream.at_eof(): + break + line = (await self.stream.readline()).decode('utf8').rstrip() + if self.target.autorecon.config['verbose'] >= 2: + if line != '': + info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} ' + line.replace('{', '{{').replace('}', '}}')) + for p in self.patterns: + matches = p.pattern.findall(line) + for match in matches: + async with self.target.lock: + with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file: + if p.description: + if self.target.autorecon.config['verbose'] >= 1: + info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}' + p.description.replace('{match}', '{bblue}' + match + '{crst}{bmagenta}') + '{rst}') + file.writelines(p.description.replace('{match}', match) + '\n\n') + else: + if self.target.autorecon.config['verbose'] >= 1: + info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}Matched Pattern: {bblue}' + match + '{rst}') + file.writelines('Matched Pattern: ' + match + '\n\n') + + if self.outfile is not None: + with open(self.outfile, 'a') as writer: + writer.write(line + '\n') + self.lines.append(line) + self.ended = True + + async def readline(self): + while True: + try: + return self.lines.pop(0) + except IndexError: + if self.ended: + return None + else: + await asyncio.sleep(0.1) + +class Plugin(object): + + def __init__(self): + self.name = None + self.slug = None + self.description = None + self.type = None + self.tags = ['default'] + self.priority = 1 + self.patterns = [] + self.match = None + self.manual_commands = {} + self.autorecon = None + self.disabled = False + + @final + def add_option(self, name, default=None, help=None): + self.autorecon.add_argument(self, name, metavar='VALUE', default=default, help=help) + + @final + def add_constant_option(self, name, const, default=None, help=None): + self.autorecon.add_argument(self, name, action='store_const', const=const, default=default, help=help) + + @final + def add_true_option(self, name, help=None): + self.autorecon.add_argument(self, name, action='store_true', help=help) + + @final + def add_false_option(self, name, help=None): + self.autorecon.add_argument(self, name, action='store_false', help=help) + + @final + def add_list_option(self, name, default=None, help=None): + self.autorecon.add_argument(self, name, action='append', metavar='VALUE', default=default, help=help) + + @final + def add_choice_option(self, name, choices, default=None, help=None): + if not isinstance(choices, list): + fail('The choices argument for ' + self.name + '\'s ' + name + ' choice option should be a list.') + self.autorecon.add_argument(self, name, choices=choices, default=default, help=help) + + @final + def get_option(self, name): + # TODO: make sure name is simple. + name = self.slug.replace('-', '_') + '.' + slugify(name).replace('-', '_') + + if name in vars(self.autorecon.args): + return vars(self.autorecon.args)[name] + else: + return None + + @final + def get_global_option(self, name): + name = 'global.' + slugify(name).replace('-', '_') + + if name in vars(self.autorecon.args): + return vars(self.autorecon.args)[name] + else: + return None + + @final + def get_global(self, name): + return self.get_global_option(name) + + @final + def add_manual_commands(self, description, commands): + if not isinstance(commands, list): + commands = [commands] + self.manual_commands[description] = commands + + @final + def add_manual_command(self, description, command): + self.add_manual_commands(description, command) + + @final + def add_pattern(self, pattern, description=None): + try: + compiled = re.compile(pattern) + if description: + self.patterns.append(Pattern(compiled, description=description)) + else: + self.patterns.append(Pattern(compiled)) + except re.error: + fail('Error: The pattern "' + pattern + '" in the plugin "' + self.name + '" is invalid regex.') + +class PortScan(Plugin): + + def __init__(self): + super().__init__() + + async def run(self, target): + raise NotImplementedError + +class ServiceScan(Plugin): + + def __init__(self): + super().__init__() + self.ports = {'tcp':[], 'udp':[]} + self.ignore_ports = {'tcp':[], 'udp':[]} + self.services = [] + self.ignore_services = [] + self.run_once_bool = False + self.require_ssl_bool = False + + @final + def add_port_match(self, protocol, port, negative_match=False): + protocol = protocol.lower() + if protocol not in ['tcp', 'udp']: + print('Invalid protocol.') + sys.exit(1) + else: + if not isinstance(port, list): + port = [port] + + port = list(map(int, port)) + + if negative_match: + self.ignore_ports[protocol] = list(set(self.ignore_ports[protocol] + port)) + else: + self.ports[protocol] = list(set(self.ports[protocol] + port)) + + @final + def add_service_match(self, regex, negative_match=False): + if not isinstance(regex, list): + regex = [regex] + + valid_regex = True + for r in regex: + try: + re.compile(r) + except re.error: + print('Invalid regex: ' + r) + valid_regex = False + + if valid_regex: + if negative_match: + self.ignore_services = list(set(self.ignore_services + regex)) + else: + self.services = list(set(self.services + regex)) + else: + sys.exit(1) + + @final + def require_ssl(self, bool): + self.require_ssl_bool = bool + + @final + def run_once(self, bool): + self.run_once_bool = bool + +class AutoRecon(object): + + def __init__(self): + self.pending_targets = [] + self.scanning_targets = [] + self.plugins = {} + self.__slug_regex = re.compile('^[a-z0-9\-]+$') + self.plugin_types = {'port':[], 'service':[]} + self.port_scan_semaphore = None + self.service_scan_semaphore = None + self.argparse = None + self.argparse_group = None + self.args = None + self.tags = [] + self.excluded_tags = [] + self.patterns = [] + self.configurable_keys = ['max_scans', 'max_port_scans', 'single_target', 'outdir', 'only_scans_dir', 'heartbeat', 'timeout', 'target_timeout', 'accessible', 'verbose'] + self.config = { + 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], + 'global_file': os.path.dirname(os.path.realpath(__file__)) + '/global.toml', + 'max_scans': 50, + 'max_port_scans': None, + 'single_target': False, + 'outdir': 'results', + 'only_scans_dir': False, + 'heartbeat': 60, + 'timeout': None, + 'target_timeout': None, + 'accessible': False, + 'verbose': 0 + } + self.lock = asyncio.Lock() + self.load_slug = None + self.load_module = None + + def add_argument(self, plugin, name, **kwargs): + # TODO: make sure name is simple. + name = '--' + plugin.slug + '.' + slugify(name) + '''if 'action' in kwargs.keys() and kwargs['action'] != 'store': + if kwargs['action'] in ['store_true'] + else: + if 'metavar' not in kwargs.keys(): + kwargs['metavar'] = 'VALUE' + + if 'metavar' not in kwargs.keys() and 'choices' not in kwargs.keys() and ('action' in kwargs.keys() and kwargs['action'] not in ['store_true', 'store_false']): + kwargs['metavar'] = 'VALUE' + ''' + + if self.argparse_group is None: + self.argparse_group = self.argparse.add_argument_group('plugin arguments', description='These are optional arguments for certain plugins.') + self.argparse_group.add_argument(name, **kwargs) + + def extract_service(self, line, regex): + if regex is None: + regex = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + match = re.search(regex, line) + if match: + protocol = match.group('protocol').lower() + port = int(match.group('port')) + service = match.group('service') + secure = True if 'ssl' in service or 'tls' in service else False + + if service.startswith('ssl/') or service.startswith('tls/'): + service = service[4:] + + from autorecon import Service + return Service(protocol, port, service, secure) + else: + return None + + async def extract_services(self, stream, regex): + if not isinstance(stream, CommandStreamReader): + print('Error: extract_services must be passed an instance of a CommandStreamReader.') + sys.exit(1) + + services = [] + while True: + line = await stream.readline() + if line is not None: + service = self.extract_service(line, regex) + if service: + services.append(service) + else: + break + return services + + def register(self, plugin): + if plugin.disabled: + return + + for _, loaded_plugin in self.plugins.items(): + if plugin.name == loaded_plugin.name: + fail('Error: Duplicate plugin name "' + plugin.name + '" detected.', file=sys.stderr) + + if plugin.slug is None: + plugin.slug = slugify(plugin.name) + elif not self.__slug_regex.match(plugin.slug): + fail('Error: provided slug "' + plugin.slug + '" is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) + + if plugin.slug in self.config['protected_classes']: + fail('Error: plugin slug "' + plugin.slug + '" is a protected string. Please change.') + + if plugin.slug not in self.plugins: + + for _, loaded_plugin in self.plugins.items(): + if plugin is loaded_plugin: + fail('Error: plugin "' + plugin.name + '" already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) + + if plugin.description is None: + plugin.description = '' + + configure_function_found = False + run_coroutine_found = False + manual_function_found = False + + for member_name, member_value in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'configure': + configure_function_found = True + elif member_name == 'run' and inspect.iscoroutinefunction(member_value): + run_coroutine_found = True + elif member_name == 'manual': + manual_function_found = True + + if not run_coroutine_found and not manual_function_found: + fail('Error: the plugin "' + plugin.name + '" needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) + + from autorecon import PortScan, ServiceScan + if issubclass(plugin.__class__, PortScan): + self.plugin_types["port"].append(plugin) + elif issubclass(plugin.__class__, ServiceScan): + self.plugin_types["service"].append(plugin) + else: + fail('Plugin "' + plugin.name + '" is neither a PortScan nor a ServiceScan.', file=sys.stderr) + + plugin.tags = [tag.lower() for tag in plugin.tags] + + plugin.autorecon = self + if configure_function_found: + plugin.configure() + self.plugins[plugin.slug] = plugin + else: + fail('Error: plugin slug "' + plugin.slug + '" is already assigned.', file=sys.stderr) + + async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None): + if patterns: + combined_patterns = self.patterns + patterns + else: + combined_patterns = self.patterns + + process = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + executable='/bin/bash') + + cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile) + cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile) + + asyncio.create_task(cout._read()) + asyncio.create_task(cerr._read()) + + return process, cout, cerr + +# Since this file is run as the main method and also imported by plugins, +# we need to make sure that only one instance of the AutoRecon is +# created. This cannot be done with Singletons unfortunately, which is +# why this hack is here. +if 'autorecon' not in sys.modules: # If this file is not yet imported, create the AutoRecon object + autorecon = AutoRecon() +else: # Otherwise, assign it from the __main__ module. + autorecon = sys.modules['__main__'].autorecon + +def e(*args, frame_index=1, **kvargs): + frame = sys._getframe(frame_index) + + vals = {} + + vals.update(frame.f_globals) + vals.update(frame.f_locals) + vals.update(kvargs) + + return string.Formatter().vformat(' '.join(args), args, vals) + +def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, **kvargs): + frame = sys._getframe(frame_index) + + vals = { + 'bgreen': Fore.GREEN + Style.BRIGHT, + 'bred': Fore.RED + Style.BRIGHT, + 'bblue': Fore.BLUE + Style.BRIGHT, + 'byellow': Fore.YELLOW + Style.BRIGHT, + 'bmagenta': Fore.MAGENTA + Style.BRIGHT, + + 'green': Fore.GREEN, + 'red': Fore.RED, + 'blue': Fore.BLUE, + 'yellow': Fore.YELLOW, + 'magenta': Fore.MAGENTA, + + 'bright': Style.BRIGHT, + 'srst': Style.NORMAL, + 'crst': Fore.RESET, + 'rst': Style.NORMAL + Fore.RESET + } + + if autorecon.config['accessible']: + vals = {'bgreen':'', 'bred':'', 'bblue':'', 'byellow':'', 'bmagenta':'', 'green':'', 'red':'', 'blue':'', 'yellow':'', 'magenta':'', 'bright':'', 'srst':'', 'crst':'', 'rst':''} + + vals.update(frame.f_globals) + vals.update(frame.f_locals) + vals.update(kvargs) + + unfmt = '' + if char is not None and not autorecon.config['accessible']: + unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep + unfmt += sep.join(args) + + fmted = unfmt + + for attempt in range(10): + try: + fmted = string.Formatter().vformat(unfmt, args, vals) + break + except KeyError as err: + key = err.args[0] + unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') + + if printmsg: + print(fmted, sep=sep, end=end, file=file) + else: + return fmted + +def debug(*args, color=Fore.GREEN, sep=' ', end='\n', file=sys.stdout, **kvargs): + if verbose >= 2: + if autorecon.config['accessible']: + args = ('Debug:',) + args + cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def info(*args, sep=' ', end='\n', file=sys.stdout, **kvargs): + cprint(*args, color=Fore.BLUE, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def warn(*args, sep=' ', end='\n', file=sys.stderr,**kvargs): + if autorecon.config['accessible']: + args = ('Warning:',) + args + cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def error(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): + if autorecon.config['accessible']: + args = ('Error:',) + args + cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): + if autorecon.config['accessible']: + args = ('Failure:',) + args + cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + exit(-1) + +def calculate_elapsed_time(start_time): + elapsed_seconds = round(time.time() - start_time) + + m, s = divmod(elapsed_seconds, 60) + h, m = divmod(m, 60) + + elapsed_time = [] + if h == 1: + elapsed_time.append(str(h) + ' hour') + elif h > 1: + elapsed_time.append(str(h) + ' hours') + + if m == 1: + elapsed_time.append(str(m) + ' minute') + elif m > 1: + elapsed_time.append(str(m) + ' minutes') + + if s == 1: + elapsed_time.append(str(s) + ' second') + elif s > 1: + elapsed_time.append(str(s) + ' seconds') + else: + elapsed_time.append('less than a second') + + return ', '.join(elapsed_time) + +def slugify(name): + return re.sub(r'[\W_]+', '-', unidecode.unidecode(name).lower()).strip('-') + +def cancel_all_tasks(signal, frame): + for task in asyncio.all_tasks(): + task.cancel() + + for target in autorecon.scanning_targets: + for process_list in target.running_tasks.values(): + for process_dict in process_list['processes']: + process_dict['process'].kill() + +async def start_heartbeat(target, period=60): + while True: + await asyncio.sleep(period) + async with target.lock: + count = len(target.running_tasks) + + tasks_list = '' + if target.autorecon.config['verbose'] >= 1: + tasks_list = ': {bblue}' + ', '.join(target.running_tasks.keys()) + '{rst}' + + current_time = datetime.now().strftime('%H:%M:%S') + + if count > 1: + info('{bgreen}' + current_time + '{rst} - There are {byellow}' + str(count) + '{rst} scans still running against {byellow}' + target.address + '{rst}' + tasks_list) + elif count == 1: + info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) + +async def port_scan(plugin, target): + async with target.autorecon.port_scan_semaphore: + info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') + + async with target.lock: + target.running_tasks[plugin.slug] = {'plugin': plugin, 'processes':[]} + + start_time = time.time() + try: + result = await plugin.run(target) + except Exception as ex: + exc_type, exc_value, exc_tb = sys.exc_info() + error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) + raise Exception(cprint('Error: Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) + + for process_dict in target.running_tasks[plugin.slug]['processes']: + if process_dict['process'].returncode is None: + warn('A process was left running after port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + await process_dict['process'].wait() + + if process_dict['process'].returncode != 0: + errors = [] + while True: + line = await process_dict['stderr'].readline() + if line is not None: + errors.append(line + '\n') + else: + break + error('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.') + async with target.lock: + with open(os.path.join(target.scandir, '_errors.log'), 'a') as file: + file.writelines('[*] Port scan ' + plugin.name + ' (' + plugin.slug + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') + file.writelines('[-] Command: ' + process_dict['cmd'] + '\n') + if errors: + file.writelines(['[-] Error Output:\n'] + errors + ['\n']) + else: + file.writelines('\n') + + elapsed_time = calculate_elapsed_time(start_time) + + async with target.lock: + target.running_tasks.pop(plugin.slug, None) + + info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time) + return {'type':'port', 'plugin':plugin, 'result':result} + +async def service_scan(plugin, service): + from autorecon import PortScan + semaphore = service.target.autorecon.service_scan_semaphore + + # If service scan semaphore is locked, see if we can use port scan semaphore. + while True: + if semaphore.locked(): + if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans + # port_scan_task_count = 0 + # for t in asyncio.all_tasks(): + # frame = inspect.getframeinfo(t.get_stack(limit=1)[0]) + # if frame.function == 'port_scan' and 'await plugin.run(target)' in frame.code_context[0]: + # #print(frame) + # port_scan_task_count += 1 + # print("Old Port Scan Task Count: " + str(port_scan_task_count)) + + port_scan_task_count = 0 + for targ in service.target.autorecon.scanning_targets: + for process_list in targ.running_tasks.values(): + #print('Length: ' + str(len(process_list))) + #for process_dict in process_list['processes']: + if issubclass(process_list['plugin'].__class__, PortScan): + #for process_dict in process_list['processes']: + # print(str(process_dict['process'].returncode) + ': ' + process_dict['cmd']) + port_scan_task_count += 1 + #print("New Port Scan Task Count: " + str(new_port_scan_task_count)) + + if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. + if service.target.autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = service.target.autorecon.port_scan_semaphore + break + else: # Do some math to see if we can use the port scan semaphore. + if (service.target.autorecon.config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * service.target.autorecon.config['port_scan_plugin_count']))) >= 1: + if service.target.autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = service.target.autorecon.port_scan_semaphore + break + else: + await asyncio.sleep(1) + else: + break + else: + break + + async with semaphore: + tag = service.tag() + '/' + plugin.slug + info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') + + async with service.target.lock: + service.target.running_tasks[tag] = {'plugin': plugin, 'processes':[]} + + start_time = time.time() + try: + result = await plugin.run(service) + except Exception as ex: + exc_type, exc_value, exc_tb = sys.exc_info() + error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) + raise Exception(cprint('Error: Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) + + for process_dict in service.target.running_tasks[tag]['processes']: + if process_dict['process'].returncode is None: + warn('A process was left running after service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + await process_dict['process'].wait() + + if process_dict['process'].returncode != 0: + errors = [] + while True: + line = await process_dict['stderr'].readline() + if line is not None: + errors.append(line + '\n') + else: + break + error('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.') + async with service.target.lock: + with open(os.path.join(service.target.scandir, '_errors.log'), 'a') as file: + file.writelines('[*] Service scan ' + plugin.name + ' (' + tag + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') + file.writelines('[-] Command: ' + process_dict['cmd'] + '\n') + if errors: + file.writelines(['[-] Error Output:\n'] + errors + ['\n']) + else: + file.writelines('\n') + + elapsed_time = calculate_elapsed_time(start_time) + + async with service.target.lock: + service.target.running_tasks.pop(tag, None) + + info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time) + return {'type':'service', 'plugin':plugin, 'result':result} + +async def scan_target(target): + if target.autorecon.config['single_target']: + basedir = os.path.abspath(target.autorecon.config['outdir']) + else: + basedir = os.path.abspath(os.path.join(target.autorecon.config['outdir'], target.address)) + target.basedir = basedir + os.makedirs(basedir, exist_ok=True) + + if not target.autorecon.config['only_scans_dir']: + exploitdir = os.path.abspath(os.path.join(basedir, 'exploit')) + os.makedirs(exploitdir, exist_ok=True) + + lootdir = os.path.abspath(os.path.join(basedir, 'loot')) + os.makedirs(lootdir, exist_ok=True) + + reportdir = os.path.abspath(os.path.join(basedir, 'report')) + target.reportdir = reportdir + os.makedirs(reportdir, exist_ok=True) + + open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close() + open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close() + + screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots')) + os.makedirs(screenshotdir, exist_ok=True) + + scandir = os.path.abspath(os.path.join(basedir, 'scans')) + target.scandir = scandir + os.makedirs(scandir, exist_ok=True) + + os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True) + + pending = [] + + heartbeat = asyncio.create_task(start_heartbeat(target, period=target.autorecon.config['heartbeat'])) + + for plugin in target.autorecon.plugin_types['port']: + plugin_tag_set = set(plugin.tags) + + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break + + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break + + if matching_tags and not excluded_tags: + pending.append(asyncio.create_task(port_scan(plugin, target))) + + async with autorecon.lock: + autorecon.scanning_targets.append(target) + + start_time = time.time() + info('Scanning target {byellow}' + target.address + '{rst}') + + timed_out = False + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + + # Check if global timeout has occurred. + if autorecon.config['target_timeout'] is not None: + elapsed_seconds = round(time.time() - start_time) + m, s = divmod(elapsed_seconds, 60) + if m >= autorecon.config['target_timeout']: + timed_out = True + break + + # Extract Services + services = [] + async with target.lock: + while target.pending_services: + services.append(target.pending_services.pop(0)) + + for task in done: + try: + if task.exception(): + print(task.exception()) + continue + except asyncio.InvalidStateError: + pass + + if task.result()['type'] == 'port': + for service in (task.result()['result'] or []): + services.append(service) + + for service in services: + if service.full_tag() not in target.services: + target.services.append(service.full_tag()) + else: + continue + + #plugin = task.result()['plugin'] + #info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){srst}]{rst} found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') + info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') + + service.target = target + + # Create variables for command references. + address = target.address + scandir = target.scandir + protocol = service.protocol + port = service.port + + # Special cases for HTTP. + http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http' + + nmap_extra = target.autorecon.args.nmap + if target.autorecon.args.nmap_append: + nmap_extra += ' ' + target.autorecon.args.nmap_append + + if protocol == 'udp': + nmap_extra += ' -sU' + + matching_plugins = [] + heading = False + + for plugin in target.autorecon.plugin_types['service']: + plugin_tag = service.tag() + '/' + plugin.slug + + for s in plugin.services: + if re.search(s, service.name): + plugin_tag_set = set(plugin.tags) + + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break + + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break + + # TODO: Maybe make this less messy, keep manual-only plugins separate? + plugin_is_runnable = False + for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'run': + plugin_is_runnable = True + break + + if plugin_is_runnable and matching_tags and not excluded_tags: + # Skip plugin if run_once_bool and plugin already in target scans + if plugin.run_once_bool and (plugin.slug,) in target.scans: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') + continue + + # Skip plugin if require_ssl_bool and port is not secure + if plugin.require_ssl_bool and not service.secure: + continue + + # Skip plugin if service port is in ignore_ports: + if port in plugin.ignore_ports[protocol]: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') + continue + + # Skip plugin if plugin has required ports and service port is not in them: + if plugin.ports[protocol] and port not in plugin.ports[protocol]: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin can only run on specific ports. Skipping.{rst}') + continue + + for i in plugin.ignore_services: + if re.search(i, service.name): + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against this service. Skipping.{rst}') + continue + + # TODO: check if plugin matches tags, BUT run manual commands anyway! + matching_plugins.append(plugin) + + if plugin.manual_commands and (not plugin.run_once_bool or (plugin.run_once_bool and (plugin.slug,) not in target.scans)): + with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: + if not heading: + file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) + heading = True + for description, commands in plugin.manual_commands.items(): + file.write('\t[-] ' + e(description) + '\n\n') + for command in commands: + file.write('\t\t' + e(command) + '\n\n') + file.flush() + + break + + for plugin in matching_plugins: + plugin_tag = service.tag() + '/' + plugin.slug + + scan_tuple = (service.protocol, service.port, service.name, plugin.slug) + if plugin.run_once_bool: + scan_tuple = (plugin.slug,) + + if scan_tuple in target.scans: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') + continue + else: + target.scans.append(scan_tuple) + + pending.add(asyncio.create_task(service_scan(plugin, service))) + heartbeat.cancel() + elapsed_time = calculate_elapsed_time(start_time) + + if timed_out: + + for task in pending: + task.cancel() + + for process_list in target.running_tasks.values(): + for process_dict in process_list['processes']: + process_dict['process'].kill() + + warn('{byellow}Scanning target ' + target.address + ' took longer than the specified target period (' + str(autorecon.config['target_timeout']) + ' min). Cancelling scans and moving to next target.{rst}') + else: + info('Finished scanning target {byellow}' + target.address + '{rst} in ' + elapsed_time) + + async with autorecon.lock: + autorecon.scanning_targets.remove(target) + +async def main(): + from autorecon import Plugin, PortScan, ServiceScan, Target # We have to do this to get around issubclass weirdness when loading plugins. + + parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') + parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') + parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') + parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: 50') + parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') + parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') + parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') + parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group.') + parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group.') + parser.add_argument('--plugins-dir', action='store', type=str, default=os.path.dirname(os.path.abspath(__file__)) + '/plugins', help='') + parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: results') + parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: false') + parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: false') + parser.add_argument('--heartbeat', action='store', type=int, help='Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60') + parser.add_argument('--timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: no timeout') + parser.add_argument('--target-timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: no timeout') + nmap_group = parser.add_mutually_exclusive_group() + nmap_group.add_argument('--nmap', action='store', default='-vv --reason -Pn', help='Override the {nmap_extra} variable in scans. Default: %(default)s') + nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.') + parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running. Default: false') + parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders.') + parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') + parser.error = lambda s: fail(s[0].upper() + s[1:]) + args, unknown = parser.parse_known_args() + + errors = False + + autorecon.argparse = parser + + # Parse config file and args for global.toml first. + if not os.path.isfile(args.config_file): + fail('Error: Specified config file "' + args.config_file + '" does not exist.') + + with open(args.config_file) as c: + try: + config_toml = toml.load(c) + for key, val in config_toml.items(): + if key.replace('-', '_') == 'global_file': + autorecon.config['global_file'] = val + break + except toml.decoder.TomlDecodeError: + fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.') + + args_dict = vars(args) + for key in args_dict: + if key == 'global_file' and args_dict['global_file'] is not None: + autorecon.config['global_file'] = args_dict['global_file'] + break + + if not os.path.isdir(args.plugins_dir): + fail('Error: Specified plugins directory "' + args.plugins_dir + '" does not exist.') + + for plugin_file in os.listdir(args.plugins_dir): + if not plugin_file.startswith('_') and plugin_file.endswith('.py'): + + dirname, filename = os.path.split(plugin_file) + dirname = os.path.abspath(dirname) + + try: + plugin = importlib.import_module('.' + filename[:-3], os.path.basename(args.plugins_dir)) + clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) + for (_, c) in clsmembers: + if c.__module__ == 'autorecon': + continue + + if c.__name__.lower() in autorecon.config['protected_classes']: + print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.') + sys.exit(1) + + # Only add classes that are a sub class of either PortScan or ServiceScan + if issubclass(c, PortScan) or issubclass(c, ServiceScan): + autorecon.register(c()) + else: + print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.') + except (ImportError, SyntaxError) as ex: + print('cannot import ' + filename + ' plugin') + print(ex) + sys.exit(1) + + if len(autorecon.plugin_types['port']) == 0: + fail('Error: There are no valid PortScan plugins in the plugins directory "' + args.plugins_dir + '".') + + # Sort plugins by priority. + autorecon.plugin_types['port'].sort(key=lambda x: x.priority) + autorecon.plugin_types['service'].sort(key=lambda x: x.priority) + + if not os.path.isfile(autorecon.config['global_file']): + fail('Error: Specified global file "' + autorecon.config['global_file'] + '" does not exist.') + + global_plugin_args = None + with open(autorecon.config['global_file']) as g: + try: + global_toml = toml.load(g) + for key, val in global_toml.items(): + if key == 'global' and isinstance(val, dict): # Process global plugin options. + for gkey, gvals in global_toml['global'].items(): + if isinstance(gvals, dict):# and 'help' in gvals: + options = {'metavar':'VALUE'} + + if 'default' in gvals: + options['default'] = gvals['default'] + + if 'metavar' in gvals: + options['metavar'] = gvals['metavar'] + + if 'help' in gvals: + options['help'] = gvals['help'] + + if 'type' in gvals: + gtype = gvals['type'].lower() + if gtype == 'constant': + if 'constant' not in gvals: + fail('Global constant option ' + gkey + ' has no constant value set.') + else: + options['action'] = 'store_const' + options['const'] = gvals['constant'] + elif gtype == 'true': + options['action'] = 'store_true' + options.pop('metavar', None) + options.pop('default', None) + elif gtype == 'false': + options['action'] = 'store_false' + options.pop('metavar', None) + options.pop('default', None) + elif gtype == 'list': + options['action'] = 'append' + elif gtype == 'choice': + if 'choices' not in gvals: + fail('Global choice option ' + gkey + ' has no choices value set.') + else: + if not isinstance(gvals['choices'], list): + fail('The \'choices\' value for global choice option ' + gkey + ' should be a list.') + options['choices'] = gvals['choices'] + options.pop('metavar', None) + + if global_plugin_args is None: + global_plugin_args = parser.add_argument_group("global plugin arguments", description="These are optional arguments that can be used by all plugins.") + + global_plugin_args.add_argument('--global.' + slugify(gkey), **options) + + except toml.decoder.TomlDecodeError: + fail('Error: Couldn\'t parse ' + g.name + ' file. Check syntax.') + + for key, val in config_toml.items(): + if key == 'global' and isinstance(val, dict): # Process global plugin options. + for gkey, gval in config_toml['global'].items(): + if isinstance(gval, bool): + for action in autorecon.argparse._actions: + if action.dest == 'global.' + slugify(gkey).replace('-', '_'): + if action.const is True: + action.__setattr__('default', gval) + break + else: + if autorecon.argparse.get_default('global.' + slugify(gkey).replace('-', '_')): + autorecon.argparse.set_defaults(**{'global.' + slugify(gkey).replace('-', '_'): gval}) + + elif key == 'pattern' and isinstance(val, list): # Process global patterns. + for pattern in val: + if 'pattern' in pattern: + try: + compiled = re.compile(pattern['pattern']) + if 'description' in pattern: + autorecon.patterns.append(Pattern(compiled, description=pattern['description'])) + else: + autorecon.patterns.append(Pattern(compiled)) + except re.error: + fail('Error: The pattern "' + pattern['pattern'] + '" in the config file is invalid regex.') + else: + fail('Error: A [[pattern]] in the config file doesn\'t have a required pattern variable.') + elif isinstance(val, dict): # Process potential plugin arguments. + for pkey, pval in config_toml[key].items(): + if autorecon.argparse.get_default(slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_')): + autorecon.argparse.set_defaults(**{slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): pval}) + else: # Process potential other options. + if key.replace('-', '_') in autorecon.configurable_keys: + autorecon.config[key.replace('-', '_')] = val + autorecon.argparse.set_defaults(**{key.replace('-', '_'): val}) + + parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='Show this help message and exit.') + args = parser.parse_args() + + args_dict = vars(args) + for key in args_dict: + if key in autorecon.configurable_keys and args_dict[key] is not None: + # Special case for booleans + if key in ['accessible', 'single_target', 'only_scans_dir'] and autorecon.config[key]: + continue + autorecon.config[key] = args_dict[key] + + autorecon.args = args + + if autorecon.config['max_scans'] <= 0: + error('Argument -m/--max-scans must be at least 1.') + errors = True + + if autorecon.config['max_port_scans'] is None: + autorecon.config['max_port_scans'] = max(1, round(autorecon.config['max_scans'] * 0.2)) + else: + if autorecon.config['max_port_scans'] <= 0: + error('Argument -mp/--max-port-scans must be at least 1.') + errors = True + + if autorecon.config['max_port_scans'] > autorecon.config['max_scans']: + error('Argument -mp/--max-port-scans cannot be greater than argument -m/--max-scans.') + errors = True + + if autorecon.config['heartbeat'] <= 0: + error('Argument --heartbeat must be at least 1.') + errors = True + + if autorecon.config['timeout'] is not None and autorecon.config['timeout'] <= 0: + error('Argument --timeout must be at least 1.') + errors = True + + if autorecon.config['target_timeout'] is not None and autorecon.config['target_timeout'] <= 0: + error('Argument --target-timeout must be at least 1.') + errors = True + + if autorecon.config['timeout'] is not None and autorecon.config['target_timeout'] is not None and autorecon.config['timeout'] < autorecon.config['target_timeout']: + error('Argument --timeout cannot be less than --target-timeout.') + errors = True + + if not errors: + autorecon.port_scan_semaphore = asyncio.Semaphore(autorecon.config['max_port_scans']) + # If max scans and max port scans is the same, the service scan semaphore and port scan semaphore should be the same object + if autorecon.config['max_scans'] == autorecon.config['max_port_scans']: + autorecon.service_scan_semaphore = autorecon.port_scan_semaphore + else: + autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans'] - autorecon.config['max_port_scans']) + + tags = [] + for tag_group in list(set(filter(None, args.tags.lower().split(',')))): + tags.append(list(set(filter(None, tag_group.split('+'))))) + + # Remove duplicate lists from list. + [autorecon.tags.append(t) for t in tags if t not in autorecon.tags] + + excluded_tags = [] + if args.exclude_tags != '': + for tag_group in list(set(filter(None, args.exclude_tags.lower().split(',')))): + excluded_tags.append(list(set(filter(None, tag_group.split('+'))))) + + # Remove duplicate lists from list. + [autorecon.excluded_tags.append(t) for t in excluded_tags if t not in autorecon.excluded_tags] + + # Generate manual commands. + for _, plugin in autorecon.plugins.items(): + for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'manual': + plugin.manual() + + raw_targets = args.targets + + if len(args.target_file) > 0: + if not os.path.isfile(args.target_file): + error('The target file ' + args.target_file + ' was not found.') + sys.exit(1) + try: + with open(args.target_file, 'r') as f: + lines = f.read() + for line in lines.splitlines(): + line = line.strip() + if line.startswith('#') or len(line) == 0: continue + if line not in raw_targets: + raw_targets.append(line) + except OSError: + error('The target file ' + args.target_file + ' could not be read.') + sys.exit(1) + + for target in raw_targets: + try: + ip = str(ipaddress.ip_address(target)) + + if ip not in autorecon.pending_targets: + autorecon.pending_targets.append(ip) + except ValueError: + + try: + target_range = ipaddress.ip_network(target, strict=False) + if not args.disable_sanity_checks and target_range.num_addresses > 256: + error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') + errors = True + else: + for ip in target_range.hosts(): + ip = str(ip) + if ip not in autorecon.pending_targets: + autorecon.pending_targets.append(ip) + except ValueError: + + try: + ip = socket.gethostbyname(target) + + if target not in autorecon.pending_targets: + autorecon.pending_targets.append(target) + except socket.gaierror: + error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') + errors = True + + if len(autorecon.pending_targets) == 0: + error('You must specify at least one target to scan!') + errors = True + + if autorecon.config['single_target'] and len(autorecon.pending_targets) != 1: + error('You cannot provide more than one target when scanning in single-target mode.') + errors = True + + if not args.disable_sanity_checks and len(autorecon.pending_targets) > 256: + error('A total of ' + str(len(autorecon.pending_targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.') + errors = True + + port_scan_plugin_count = 0 + for plugin in autorecon.plugin_types['port']: + matching_tags = False + for tag_group in autorecon.tags: + if set(tag_group).issubset(set(plugin.tags)): + matching_tags = True + break + + excluded_tags = False + for tag_group in autorecon.excluded_tags: + if set(tag_group).issubset(set(plugin.tags)): + excluded_tags = True + break + + if matching_tags and not excluded_tags: + port_scan_plugin_count += 1 + + if port_scan_plugin_count == 0: + error('There are no port scan plugins that match the tags specified.') + errors = True + + if errors: + sys.exit(1) + + autorecon.config['port_scan_plugin_count'] = port_scan_plugin_count + + num_initial_targets = max(1, math.ceil(autorecon.config['max_port_scans'] / port_scan_plugin_count)) + + start_time = time.time() + + #sys.exit(0) + + pending = [] + i = 0 + while autorecon.pending_targets: + pending.append(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + i+=1 + if i >= num_initial_targets: + break + + timed_out = False + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + # Check if global timeout has occurred. + if autorecon.config['timeout'] is not None: + elapsed_seconds = round(time.time() - start_time) + m, s = divmod(elapsed_seconds, 60) + if m >= autorecon.config['timeout']: + timed_out = True + break + + for task in done: + if autorecon.pending_targets: + pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + + #port_scan_task_count = 0 + #for t in asyncio.all_tasks(): + # if inspect.getframeinfo(t.get_stack(limit=1)[0]).function == 'port_scan': + # port_scan_task_count += 1 + #print("Old Port Scan Task Count: " + str(port_scan_task_count)) + + port_scan_task_count = 0 + for targ in autorecon.scanning_targets: + for process_list in targ.running_tasks.values(): + if issubclass(process_list['plugin'].__class__, PortScan): + #print(process_list) + port_scan_task_count += 1 + #print("New Port Scan Task Count: " + str(port_scan_task_count)) + + num_new_targets = math.ceil((autorecon.config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count) + if num_new_targets > 0: + i = 0 + while autorecon.pending_targets: + pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + i+=1 + if i >= num_new_targets: + break + + if timed_out: + cancel_all_tasks(None, None) + + elapsed_time = calculate_elapsed_time(start_time) + warn('{byellow}AutoRecon took longer than the specified timeout period (' + str(autorecon.config['timeout']) + ' min). Cancelling all scans and exiting.{rst}') + sys.exit(0) + else: + while len(asyncio.all_tasks()) > 1: # this code runs in the main() task so it will be the only task left running + await asyncio.sleep(1) + + elapsed_time = calculate_elapsed_time(start_time) + info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}') + +if __name__ == '__main__': + signal.signal(signal.SIGINT, cancel_all_tasks) + try: + asyncio.run(main()) + except asyncio.exceptions.CancelledError: + pass diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..f343a8d --- /dev/null +++ b/config.toml @@ -0,0 +1,20 @@ +# Configure regular AutoRecon options at the top of this file. + +# verbose = 1 +# max-scans = 1 + +# Configure global pattern matching here. +[[pattern]] +description = 'Nmap script found a potential vulnerability. ({match})' +pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' + +[[pattern]] +pattern = '(?i)unauthorized' + +# Configure global options here. +# [global] +# username-wordlist = '/usr/share/seclists/Usernames/cirt-default-usernames.txt' + +# Configure plugin options here. +# [dirbuster] +# wordlist = ['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web-Content/big.txt', '/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt'] diff --git a/global.toml b/global.toml new file mode 100644 index 0000000..47c7edc --- /dev/null +++ b/global.toml @@ -0,0 +1,7 @@ +[global.username-wordlist] +default = '/usr/share/seclists/Usernames/top-usernames-shortlist.txt' +help = 'A wordlist of usernames, useful for bruteforcing. Default: %(default)s' + +[global.password-wordlist] +default = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' +help = 'A wordlist of passwords, useful for bruteforcing. Default: %(default)s' diff --git a/src/autorecon/__init__.py b/plugins/__init__.py similarity index 100% rename from src/autorecon/__init__.py rename to plugins/__init__.py diff --git a/plugins/databases.py b/plugins/databases.py new file mode 100644 index 0000000..7930bb2 --- /dev/null +++ b/plugins/databases.py @@ -0,0 +1,122 @@ +from autorecon import ServiceScan + +class NmapMongoDB(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MongoDB" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match('^mongod') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mongodb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mongodb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mongodb_nmap.xml" {address}') + +class NmapMSSQL(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MSSQL" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match(['^mssql', '^ms\-sql']) + + def manual(self): + self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}') + +class NmapMYSQL(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MYSQL" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match('^mysql') + + def manual(self): + self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}') + +class NmapOracle(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Oracle" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match('^oracle') + + def manual(self): + self.add_manual_command('Brute-force SIDs using Nmap:', 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}') + +class OracleTNScmd(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle TNScmd" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match('^oracle') + + async def run(self, service): + await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') + await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') + +class OracleScanner(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle Scanner" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match('^oracle') + + async def run(self, service): + await service.execute('oscanner -v -s {address} -P {port} 2>&1', outfile='{protocol}_{port}_oracle_scanner.txt') + +class OracleODAT(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle ODAT" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match('^oracle') + + def manual(self): + self.add_manual_commands('Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:', [ + 'python odat.py tnscmd -s {address} -p {port} --ping', + 'python odat.py tnscmd -s {address} -p {port} --version', + 'python odat.py tnscmd -s {address} -p {port} --status', + 'python odat.py sidguesser -s {address} -p {port}', + 'python odat.py passwordguesser -s {address} -p {port} -d --accounts-file accounts/accounts_multiple.txt', + 'python odat.py tnspoison -s {address} -p {port} -d --test-module' + ]) + +class OraclePatator(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle Patator" + self.tags = ['default', 'databases'] + + def configure(self): + self.add_service_match('^oracle') + + def manual(self): + self.add_manual_command('Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:', 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000') diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py new file mode 100644 index 0000000..f9725f8 --- /dev/null +++ b/plugins/default-port-scan.py @@ -0,0 +1,46 @@ +from autorecon import PortScan, error +import os + +class QuickTCPPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = "Top TCP Ports" + self.tags = ["default", "default-port-scan"] + self.priority = 0 + + async def run(self, target): + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + services = await target.extract_services(stdout) + await process.wait() + return services + +class AllTCPPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = "All TCP Ports" + self.tags = ["default", "default-port-scan", "long"] + + async def run(self, target): + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + services = await target.extract_services(stdout) + await process.wait() + return services + +class Top20UDPPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = "Top 100 UDP Ports" + self.tags = ["default", "default-port-scan"] + + async def run(self, target): + # Only run UDP scan if user is root. + if os.getuid() == 0: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all --top-ports 100 -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}', blocking=False) + services = await target.extract_services(stdout) + await process.wait() + return services + else: + error('UDP scan requires AutoRecon be run with root privileges.') diff --git a/plugins/dns.py b/plugins/dns.py new file mode 100644 index 0000000..df4565c --- /dev/null +++ b/plugins/dns.py @@ -0,0 +1,14 @@ +from autorecon import ServiceScan + +class DNS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "DNS" + self.tags = ['default', 'dns'] + + def configure(self): + self.add_service_match('^domain') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}') diff --git a/plugins/ftp.py b/plugins/ftp.py new file mode 100644 index 0000000..22c49be --- /dev/null +++ b/plugins/ftp.py @@ -0,0 +1,30 @@ +from autorecon import ServiceScan + +class NmapFTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap FTP' + self.tags = ['default', 'ftp'] + + def configure(self): + self.add_service_match(['^ftp', '^ftp\-data']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}') + +class BruteforceFTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce FTP" + self.tags = ['default', 'ftp'] + + def configure(self): + self.add_service_match(['^ftp', '^ftp\-data']) + + def manual(self): + self.add_manual_commands('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' + ]) diff --git a/plugins/http.py b/plugins/http.py new file mode 100644 index 0000000..4e3eddd --- /dev/null +++ b/plugins/http.py @@ -0,0 +1,186 @@ +from autorecon import ServiceScan, error +from shutil import which +import os + +class NmapHTTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap HTTP" + self.tags = ['default', 'http'] + + def configure(self): + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + self.add_pattern('Server: ([^\n]+)', description='Identified HTTP Server: {match}') + self.add_pattern('WebDAV is ENABLED', description='WebDAV is enabled') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(http* or ssl*) and not (brute or broadcast or dos or external or http-slowloris* or fuzzer)" -oN "{scandir}/{protocol}_{port}_{http_scheme}_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_{http_scheme}_nmap.xml" {address}') + +class BruteforceHTTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce HTTP" + self.tags = ['default', 'http'] + + def configure(self): + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + + def manual(self): + self.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{address}/path/to/auth/area', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' + ]) + +class Curl(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Curl" + self.tags = ['default', 'http'] + + def configure(self): + self.add_option("path", default="/", help="The path on the web server to curl. Default: %(default)s") + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + self.add_pattern('(?i)Powered by [^\n]+') + + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('curl -sSik {http_scheme}://{address}:{port}' + self.get_option('path'), outfile='{protocol}_{port}_{http_scheme}_curl.html') + +class CurlRobots(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Curl Robots" + self.tags = ['default', 'http'] + + def configure(self): + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('curl -sSik {http_scheme}://{address}:{port}/robots.txt', outfile='{protocol}_{port}_{http_scheme}_curl-robots.txt') + +class DirBuster(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "DirBuster" + self.slug = 'dirbuster' + self.priority = 0 + self.tags = ['default', 'http', 'long'] + + def configure(self): + self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') + self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist to use when directory busting. Specify the option multiple times to use multiple wordlists. Default: %(default)s') + self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + + def manual(self): + self.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', + 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' + ]) + + self.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + ]) + + self.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', + 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' + ]) + + self.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirb {http_scheme}://{address}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_big.txt"', + 'dirb {http_scheme}://{address}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' + ]) + + self.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + ]) + + async def run(self, service): + for wordlist in self.get_option('wordlist'): + name = os.path.splitext(os.path.basename(wordlist))[0] + if self.get_option('tool') == 'feroxbuster': + await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "txt,html,php,asp,aspx,jsp" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') + elif self.get_option('tool') == 'gobuster': + await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') + elif self.get_option('tool') == 'dirsearch': + await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w ' + wordlist + ' --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') + elif self.get_option('tool') == 'ffuf': + await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e ".txt,.html,.php,.asp,.aspx,.jsp" -v | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') + elif self.get_option('tool') == 'dirb': + await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') + +class Nikto(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'nikto' + self.tags = ['default', 'http', 'long'] + + def configure(self): + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + + def manual(self): + self.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') + +class WhatWeb(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "whatweb" + self.tags = ['default', 'http'] + + def configure(self): + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('whatweb --color=never --no-errors -a 3 -v {http_scheme}://{address}:{port} 2>&1', outfile='{protocol}_{port}_{http_scheme}_whatweb.txt') + +class WkHTMLToImage(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "wkhtmltoimage" + self.tags = ['default', 'http'] + + def configure(self): + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + + async def run(self, service): + if which('wkhtmltoimage') is not None and service.protocol == 'tcp': + await service.execute('wkhtmltoimage --format png {http_scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') + else: + error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') + +class WPScan(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'WPScan' + self.tags = ['default', 'http'] + + def configure(self): + self.add_service_match('^http') + self.add_service_match('^nacn_http$', negative_match=True) + + def manual(self): + self.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') diff --git a/plugins/kerberos.py b/plugins/kerberos.py new file mode 100644 index 0000000..b093bbc --- /dev/null +++ b/plugins/kerberos.py @@ -0,0 +1,14 @@ +from autorecon import ServiceScan + +class NmapKerberos(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Kerberos" + self.tags = ['default', 'kerberos', 'active-directory'] + + def configure(self): + self.add_service_match(['^kerberos', '^kpasswd']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') diff --git a/plugins/ldap.py b/plugins/ldap.py new file mode 100644 index 0000000..39e88dd --- /dev/null +++ b/plugins/ldap.py @@ -0,0 +1,29 @@ +from autorecon import ServiceScan + +class NmapLDAP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap LDAP" + self.tags = ['default', 'ldap', 'active-directory'] + + def configure(self): + self.add_service_match('^ldap') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ldap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ldap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ldap_nmap.xml" {address}') + +class LDAPSearch(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'LDAP Search' + self.tags = ['default', 'ldap', 'active-directory'] + + def configure(self): + self.add_service_match('^ldap') + + def manual(self): + self.add_manual_command('ldapsearch command (modify before running):', [ + 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' + ]) diff --git a/plugins/misc.py b/plugins/misc.py new file mode 100644 index 0000000..0e4144a --- /dev/null +++ b/plugins/misc.py @@ -0,0 +1,170 @@ +from autorecon import ServiceScan + +class NmapCassandra(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Cassandra" + self.tags = ['default', 'cassandra'] + + def configure(self): + self.add_service_match('^apani1') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cassandra* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cassandra_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cassandra_nmap.xml" {address}') + +class NmapCUPS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap CUPS" + self.tags = ['default', 'cups'] + + def configure(self): + self.add_service_match('^ipp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cups* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cups_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cups_nmap.xml" {address}') + +class NmapDistccd(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap distccd" + self.tags = ['default', 'distccd'] + + def configure(self): + self.add_service_match('^distccd') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,distcc-cve2004-2687" --script-args="distcc-cve2004-2687.cmd=id" -oN "{scandir}/{protocol}_{port}_distcc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_distcc_nmap.xml" {address}') + +class NmapFinger(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap finger" + self.tags = ['default', 'finger'] + + def configure(self): + self.add_service_match('^finger') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,finger" -oN "{scandir}/{protocol}_{port}_finger_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_finger_nmap.xml" {address}') + +class NmapIMAP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap IMAP" + self.tags = ['default', 'imap', 'email'] + + def configure(self): + self.add_service_match('^imap') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}') + +class NmapNNTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap NNTP" + self.tags = ['default', 'nntp'] + + def configure(self): + self.add_service_match('^nntp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nntp-ntlm-info" -oN "{scandir}/{protocol}_{port}_nntp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nntp_nmap.xml" {address}') + +class NmapPOP3(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap POP3" + self.tags = ['default', 'pop3', 'email'] + + def configure(self): + self.add_service_match('^pop3') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(pop3* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_pop3_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_pop3_nmap.xml" {address}') + +class NmapRMI(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap RMI" + self.tags = ['default', 'rmi'] + + def configure(self): + self.add_service_match(['^java\-rmi', '^rmiregistry']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}') + +class NmapSMTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SMTP" + self.tags = ['default', 'smtp', 'email'] + + def configure(self): + self.add_service_match('^smtp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}') + +class SMTPUserEnum(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'SMTP-User-Enum' + self.tags = ['default', 'smtp', 'email'] + + def configure(self): + self.add_service_match('^smtp') + + async def run(self, service): + await service.execute('smtp-user-enum -M VRFY -U "' + self.get_global('username_wordlist') + '" -t {address} -p {port} 2>&1', outfile='{protocol}_{port}_smtp_user-enum.txt') + +class NmapTelnet(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Telnet' + self.tags = ['default', 'telnet'] + + def configure(self): + self.add_service_match('^telnet') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,telnet-encryption,telnet-ntlm-info" -oN "{scandir}/{protocol}_{port}_telnet-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_telnet_nmap.xml" {address}') + +class NmapTFTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap TFTP' + self.tags = ['default', 'tftp'] + + def configure(self): + self.add_service_match('^tftp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,tftp-enum" -oN "{scandir}/{protocol}_{port}_tftp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_tftp_nmap.xml" {address}') + +class NmapVNC(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap VNC' + self.tags = ['default', 'vnc'] + + def configure(self): + self.add_service_match('^vnc') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}') diff --git a/plugins/nfs.py b/plugins/nfs.py new file mode 100644 index 0000000..8210580 --- /dev/null +++ b/plugins/nfs.py @@ -0,0 +1,27 @@ +from autorecon import ServiceScan + +class NmapNFS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap NFS" + self.tags = ['default', 'nfs'] + + def configure(self): + self.add_service_match(['^nfs', '^rpcbind']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rpcinfo or nfs*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_nfs_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nfs_nmap.xml" {address}') + +class Showmount(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "showmount" + self.tags = ['default', 'nfs'] + + def configure(self): + self.add_service_match(['^nfs', '^rpcbind']) + + async def run(self, service): + await service.execute('showmount -e {address} 2>&1', outfile='{protocol}_{port}_showmount.txt') diff --git a/plugins/rdp.py b/plugins/rdp.py new file mode 100644 index 0000000..a4dc5cb --- /dev/null +++ b/plugins/rdp.py @@ -0,0 +1,30 @@ +from autorecon import ServiceScan + +class NmapRDP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap RDP" + self.tags = ['default', 'rdp'] + + def configure(self): + self.add_service_match(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rdp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rdp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rdp_nmap.xml" {address}') + +class BruteforceRDP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce RDP" + self.tags = ['default', 'rdp'] + + def configure(self): + self.add_service_match(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) + + def manual(self): + self.add_manual_commands('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' + ]) diff --git a/plugins/rpc.py b/plugins/rpc.py new file mode 100644 index 0000000..63535dd --- /dev/null +++ b/plugins/rpc.py @@ -0,0 +1,27 @@ +from autorecon import ServiceScan + +class NmapMSRPC(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MSRPC" + self.tags = ['default', 'rpc'] + + def configure(self): + self.add_service_match(['^msrpc', '^rpcbind', '^erpc']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,msrpc-enum,rpc-grind,rpcinfo" -oN "{scandir}/{protocol}_{port}_rpc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rpc_nmap.xml" {address}') + +class RPCClient(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "rpcclient" + self.tags = ['default', 'rpc'] + + def configure(self): + self.add_service_match(['^msrpc', '^rpcbind', '^erpc']) + + def manual(self): + self.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') diff --git a/plugins/sip.py b/plugins/sip.py new file mode 100644 index 0000000..da93233 --- /dev/null +++ b/plugins/sip.py @@ -0,0 +1,27 @@ +from autorecon import ServiceScan + +class NmapSIP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SIP" + self.tags = ['default', 'sip'] + + def configure(self): + self.add_service_match('^asterisk') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,sip-enum-users,sip-methods" -oN "{scandir}/{protocol}_{port}_sip_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_sip_nmap.xml" {address}') + +class SIPVicious(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SIPVicious" + self.tags = ['default', 'sip'] + + def configure(self): + self.add_service_match('^asterisk') + + def manual(self): + self.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') diff --git a/plugins/smb.py b/plugins/smb.py new file mode 100644 index 0000000..40b84a2 --- /dev/null +++ b/plugins/smb.py @@ -0,0 +1,85 @@ +from autorecon import ServiceScan + +class NmapSMB(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SMB" + self.tags = ['default', 'smb', 'active-directory'] + + def configure(self): + self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) + + def manual(self): + self.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' + ]) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}') + +class Enum4Linux(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Enum4Linux" + self.tags = ['default', 'enum4linux', 'active-directory'] + + def configure(self): + self.add_service_match(['^ldap', '^smb', '^microsoft\-ds', '^netbios']) + self.add_port_match('tcp', [139, 389, 445]) + self.add_port_match('udp', 137) + self.run_once(True) + + async def run(self, service): + await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') + +class NBTScan(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "nbtscan" + self.tags = ['default', 'netbios', 'active-directory'] + + def configure(self): + self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) + self.add_port_match('udp', 137) + self.run_once(True) + + async def run(self, service): + await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') + +class SMBClient(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SMBClient" + self.tags = ['default', 'smb', 'active-directory'] + + def configure(self): + self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) + self.add_port_match('tcp', [139, 445]) + self.run_once(True) + + async def run(self, service): + await service.execute('smbclient -L\\\\ -N -I {address} 2>&1', outfile='smbclient.txt') + +class SMBMap(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SMBMap" + self.tags = ['default', 'smb', 'active-directory'] + + def configure(self): + self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) + + async def run(self, service): + await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') diff --git a/plugins/snmp.py b/plugins/snmp.py new file mode 100644 index 0000000..83d552f --- /dev/null +++ b/plugins/snmp.py @@ -0,0 +1,52 @@ +from autorecon import ServiceScan + +class NmapSNMP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SNMP" + self.tags = ['default', 'snmp'] + + def configure(self): + self.add_service_match('^snmp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(snmp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_snmp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_snmp_nmap.xml" {address}') + +class OneSixtyOne(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "OneSixtyOne" + self.tags = ['default', 'snmp'] + + def configure(self): + self.add_service_match('^snmp') + self.add_port_match('udp', 161) + self.run_once(True) + self.add_option('community-strings', default='/usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt', help='The file containing a list of community strings to try. Default: %(default)s') + + async def run(self, service): + await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') + +class SNMPWalk(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SNMPWalk" + self.tags = ['default', 'snmp'] + + def configure(self): + self.add_service_match('^snmp') + self.add_port_match('udp', 161) + self.run_once(True) + + async def run(self, service): + await service.execute('snmpwalk -c public -v 1 {address} 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.1.6.0 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_system_processes.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.2 2>&1', outfile='{scandir}/{protocol}_{port}_snmp_snmpwalk_running_processes.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_process_paths.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_storage_units.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_software_names.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.4.1.77.1.2.25 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_user_accounts.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.6.13.1.3 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_tcp_ports.txt') diff --git a/plugins/ssh.py b/plugins/ssh.py new file mode 100644 index 0000000..637c390 --- /dev/null +++ b/plugins/ssh.py @@ -0,0 +1,30 @@ +from autorecon import ServiceScan + +class NmapSSH(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SSH" + self.tags = ['default', 'ssh'] + + def configure(self): + self.add_service_match('^ssh') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,ssh2-enum-algos,ssh-hostkey,ssh-auth-methods" -oN "{scandir}/{protocol}_{port}_ssh_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ssh_nmap.xml" {address}') + +class BruteforceSSH(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce SSH" + self.tags = ['default', 'ssh'] + + def configure(self): + self.add_service_match('ssh') + + def manual(self): + self.add_manual_command('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' + ]) diff --git a/plugins/sslscan.py b/plugins/sslscan.py new file mode 100644 index 0000000..c9a8833 --- /dev/null +++ b/plugins/sslscan.py @@ -0,0 +1,16 @@ +from autorecon import ServiceScan + +class SSLScan(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SSL Scan" + self.tags = ['default', 'ssl', 'tls'] + + def configure(self): + self.add_service_match('.+') + self.require_ssl(True) + + async def run(self, service): + if service.protocol == 'tcp' and service.secure: + await service.execute('sslscan --show-certificate --no-colour {address}:{port} 2>&1', outfile='{protocol}_{port}_sslscan.html') diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 75da55b..0000000 --- a/poetry.lock +++ /dev/null @@ -1,42 +0,0 @@ -[[package]] -category = "main" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -name = "appdirs" -optional = false -python-versions = "*" -version = "1.4.3" - -[[package]] -category = "main" -description = "Cross-platform colored terminal text." -name = "colorama" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" - -[[package]] -category = "main" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" -optional = false -python-versions = "*" -version = "0.10.0" - -[metadata] -content-hash = "7359d313673568959d8011a312ea74ee6c3c3f3a2a5d81f690b87f6236d051ec" -python-versions = "^3.7" - -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, - {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, -] -colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -toml = [ - {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, - {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, - {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, -] diff --git a/poetry.toml b/poetry.toml deleted file mode 100644 index 3b549d6..0000000 --- a/poetry.toml +++ /dev/null @@ -1,2 +0,0 @@ -[virtualenvs] -create = true diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 1c8cd46..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,28 +0,0 @@ -[tool.poetry] -name = "autorecon" -version = "1.0.0" -description = "A multi-threaded network reconaissance tool which performs automated enumeration of services" -authors = ["Tib3rius"] -license = "GPL-3.0-only" -readme = "README.md" -homepage = "https://github.com/Tib3rius/AutoRecon" -repository = "https://github.com/Tib3rius/AutoRecon" -packages = [ - {include = "autorecon", from = "src"}, -] - -[tool.poetry.dependencies] -python = "^3.7" -toml = "^0.10.0" -colorama = "^0.4.3" -appdirs = "^1.4.3" - -[tool.poetry.dev-dependencies] - - -[tool.poetry.scripts] -autorecon = 'autorecon.autorecon:main' - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" diff --git a/requirements.txt b/requirements.txt index d5cbce5..d43a59e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -colorama +unidecode toml -appdirs +colorama \ No newline at end of file diff --git a/src/autorecon/autorecon.py b/src/autorecon/autorecon.py deleted file mode 100755 index e9bfd78..0000000 --- a/src/autorecon/autorecon.py +++ /dev/null @@ -1,885 +0,0 @@ -#!/usr/bin/env python3 -# -# AutoRecon is a multi-threaded network reconnaissance tool which performs automated enumeration of services. -# -# This program can be redistributed and/or modified under the terms of the -# GNU General Public License, either version 3 of the License, or (at your -# option) any later version. -# - -import atexit -import argparse -import asyncio -import colorama -from colorama import Fore, Style -from concurrent.futures import ProcessPoolExecutor, as_completed, FIRST_COMPLETED -from datetime import datetime -import ipaddress -import os -import re -import socket -import string -import sys -import time -import toml -import termios -import appdirs -import shutil - -# Globals -verbose = 0 -nmap = "-vv --reason -Pn" -srvname = "" -heartbeat_interval = 60 -port_scan_profile = None -port_scan_profiles_config = None -service_scans_config = None -global_patterns = [] -username_wordlist = "/usr/share/seclists/Usernames/top-usernames-shortlist.txt" -password_wordlist = "/usr/share/seclists/Passwords/darkweb2017-top100.txt" -single_target = False -only_scans_dir = False - - -def _quit(): - TERM_FLAGS = termios.tcgetattr(sys.stdin.fileno()) - termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, TERM_FLAGS) - - -def _init(): - global port_scan_profiles_config - global service_scans_config - global global_patterns - - atexit.register(_quit) - appname = "AutoRecon" - rootdir = os.path.dirname(os.path.realpath(__file__)) - default_config_dir = os.path.join(rootdir, "config") - config_dir = appdirs.user_config_dir(appname) - port_scan_profiles_config_file = os.path.join(config_dir, "port-scan-profiles.toml") - service_scans_config_file = os.path.join(config_dir, "service-scans.toml") - global_patterns_config_file = os.path.join(config_dir, "global-patterns.toml") - - # Confirm this directory exists; if not, populate it with the default configurations - if not os.path.exists(config_dir): - os.makedirs(config_dir, exist_ok=True) - shutil.copy( - os.path.join(default_config_dir, "port-scan-profiles-default.toml"), - port_scan_profiles_config_file, - ) - shutil.copy( - os.path.join(default_config_dir, "service-scans-default.toml"), - service_scans_config_file, - ) - shutil.copy( - os.path.join(default_config_dir, "global-patterns-default.toml"), - global_patterns_config_file, - ) - - - with open(port_scan_profiles_config_file, "r") as p: - try: - port_scan_profiles_config = toml.load(p) - - if len(port_scan_profiles_config) == 0: - fail( - "There do not appear to be any port scan profiles configured in the {port_scan_profiles_config_file} config file." - ) - - except toml.decoder.TomlDecodeError as e: - fail( - "Error: Couldn't parse {port_scan_profiles_config_file} config file. Check syntax and duplicate tags." - ) - - with open(service_scans_config_file, "r") as c: - try: - service_scans_config = toml.load(c) - except toml.decoder.TomlDecodeError as e: - fail( - "Error: Couldn't parse service-scans.toml config file. Check syntax and duplicate tags." - ) - - with open(global_patterns_config_file, "r") as p: - try: - global_patterns = toml.load(p) - if "pattern" in global_patterns: - global_patterns = global_patterns["pattern"] - else: - global_patterns = [] - except toml.decoder.TomlDecodeError as e: - fail( - "Error: Couldn't parse global-patterns.toml config file. Check syntax and duplicate tags." - ) - - if "username_wordlist" in service_scans_config: - if isinstance(service_scans_config["username_wordlist"], str): - username_wordlist = service_scans_config["username_wordlist"] - - if "password_wordlist" in service_scans_config: - if isinstance(service_scans_config["password_wordlist"], str): - password_wordlist = service_scans_config["password_wordlist"] - - -def e(*args, frame_index=1, **kvargs): - frame = sys._getframe(frame_index) - - vals = {} - - vals.update(frame.f_globals) - vals.update(frame.f_locals) - vals.update(kvargs) - - return string.Formatter().vformat(' '.join(args), args, vals) - -def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, **kvargs): - frame = sys._getframe(frame_index) - - vals = { - 'bgreen': Fore.GREEN + Style.BRIGHT, - 'bred': Fore.RED + Style.BRIGHT, - 'bblue': Fore.BLUE + Style.BRIGHT, - 'byellow': Fore.YELLOW + Style.BRIGHT, - 'bmagenta': Fore.MAGENTA + Style.BRIGHT, - - 'green': Fore.GREEN, - 'red': Fore.RED, - 'blue': Fore.BLUE, - 'yellow': Fore.YELLOW, - 'magenta': Fore.MAGENTA, - - 'bright': Style.BRIGHT, - 'srst': Style.NORMAL, - 'crst': Fore.RESET, - 'rst': Style.NORMAL + Fore.RESET - } - - vals.update(frame.f_globals) - vals.update(frame.f_locals) - vals.update(kvargs) - - unfmt = '' - if char is not None: - unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep - unfmt += sep.join(args) - - fmted = unfmt - - for attempt in range(10): - try: - fmted = string.Formatter().vformat(unfmt, args, vals) - break - except KeyError as err: - key = err.args[0] - unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') - - print(fmted, sep=sep, end=end, file=file) - -def debug(*args, color=Fore.BLUE, sep=' ', end='\n', file=sys.stdout, **kvargs): - if verbose >= 2: - cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def info(*args, sep=' ', end='\n', file=sys.stdout, **kvargs): - cprint(*args, color=Fore.GREEN, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def warn(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): - cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def error(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): - cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): - cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) - exit(-1) - -def calculate_elapsed_time(start_time): - elapsed_seconds = round(time.time() - start_time) - - m, s = divmod(elapsed_seconds, 60) - h, m = divmod(m, 60) - - elapsed_time = [] - if h == 1: - elapsed_time.append(str(h) + ' hour') - elif h > 1: - elapsed_time.append(str(h) + ' hours') - - if m == 1: - elapsed_time.append(str(m) + ' minute') - elif m > 1: - elapsed_time.append(str(m) + ' minutes') - - if s == 1: - elapsed_time.append(str(s) + ' second') - elif s > 1: - elapsed_time.append(str(s) + ' seconds') - else: - elapsed_time.append('less than a second') - - return ', '.join(elapsed_time) - - -async def read_stream(stream, target, tag='?', patterns=[], color=Fore.BLUE): - address = target.address - while True: - line = "" - try: - line = await stream.readline() - except ValueError: - continue - - if line: - line = str(line.rstrip(), 'utf8', 'ignore') - debug(color + '[' + Style.BRIGHT + address + ' ' + tag + Style.NORMAL + '] ' + Fore.RESET + '{line}', color=color) - - for p in global_patterns: - matches = re.findall(p['pattern'], line) - if 'description' in p: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - ' + p['description'] + '\n\n')) - else: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) - - for p in patterns: - matches = re.findall(p['pattern'], line) - if 'description' in p: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - ' + p['description'] + '\n\n')) - else: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) - else: - break - -async def run_cmd(semaphore, cmd, target, tag='?', patterns=[]): - async with semaphore: - address = target.address - scandir = target.scandir - - info('Running task {bgreen}{tag}{rst} on {byellow}{address}{rst}' + (' with {bblue}{cmd}{rst}' if verbose >= 1 else '')) - - async with target.lock: - with open(os.path.join(scandir, '_commands.log'), 'a') as file: - file.writelines(e('{cmd}\n\n')) - - start_time = time.time() - process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') - async with target.lock: - target.running_tasks.append(tag) - - await asyncio.wait([ - read_stream(process.stdout, target, tag=tag, patterns=patterns), - read_stream(process.stderr, target, tag=tag, patterns=patterns, color=Fore.RED) - ]) - - await process.wait() - async with target.lock: - target.running_tasks.remove(tag) - elapsed_time = calculate_elapsed_time(start_time) - - if process.returncode != 0: - error('Task {bred}{tag}{rst} on {byellow}{address}{rst} returned non-zero exit code: {process.returncode}') - async with target.lock: - with open(os.path.join(scandir, '_errors.log'), 'a') as file: - file.writelines(e('[*] Task {tag} returned non-zero exit code: {process.returncode}. Command: {cmd}\n')) - else: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} finished successfully in {elapsed_time}') - - return {'returncode': process.returncode, 'name': 'run_cmd'} - -async def parse_port_scan(stream, tag, target, pattern): - address = target.address - ports = [] - - while True: - line = await stream.readline() - if line: - line = str(line.rstrip(), 'utf8', 'ignore') - debug(Fore.BLUE + '[' + Style.BRIGHT + address + ' ' + tag + Style.NORMAL + '] ' + Fore.RESET + '{line}', color=Fore.BLUE) - - parse_match = re.search(pattern, line) - if parse_match: - ports.append(parse_match.group('port')) - - - for p in global_patterns: - matches = re.findall(p['pattern'], line) - if 'description' in p: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - ' + p['description'] + '\n\n')) - else: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) - else: - break - - return ports - -async def parse_service_detection(stream, tag, target, pattern): - address = target.address - services = [] - - while True: - line = await stream.readline() - if line: - line = str(line.rstrip(), 'utf8', 'ignore') - debug(Fore.BLUE + '[' + Style.BRIGHT + address + ' ' + tag + Style.NORMAL + '] ' + Fore.RESET + '{line}', color=Fore.BLUE) - - parse_match = re.search(pattern, line) - if parse_match: - services.append((parse_match.group('protocol').lower(), int(parse_match.group('port')), parse_match.group('service'))) - - for p in global_patterns: - matches = re.findall(p['pattern'], line) - if 'description' in p: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}' + p['description'].replace('{match}', '{bblue}{match}{crst}{bmagenta}') + '{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - ' + p['description'] + '\n\n')) - else: - for match in matches: - if verbose >= 1: - info('Task {bgreen}{tag}{rst} on {byellow}{address}{rst} - {bmagenta}Matched Pattern: {bblue}{match}{rst}') - async with target.lock: - with open(os.path.join(target.scandir, '_patterns.log'), 'a') as file: - file.writelines(e('{tag} - Matched Pattern: {match}\n\n')) - else: - break - - return services - -async def run_portscan(semaphore, tag, target, service_detection, port_scan=None): - async with semaphore: - - address = target.address - scandir = target.scandir - nmap_extra = nmap - - ports = '' - if port_scan is not None: - command = e(port_scan[0]) - pattern = port_scan[1] - - info('Running port scan {bgreen}{tag}{rst} on {byellow}{address}{rst}' + (' with {bblue}{command}{rst}' if verbose >= 1 else '')) - - async with target.lock: - with open(os.path.join(scandir, '_commands.log'), 'a') as file: - file.writelines(e('{command}\n\n')) - - start_time = time.time() - process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') - async with target.lock: - target.running_tasks.append(tag) - - output = [ - parse_port_scan(process.stdout, tag, target, pattern), - read_stream(process.stderr, target, tag=tag, color=Fore.RED) - ] - - results = await asyncio.gather(*output) - - await process.wait() - async with target.lock: - target.running_tasks.remove(tag) - elapsed_time = calculate_elapsed_time(start_time) - - if process.returncode != 0: - error('Port scan {bred}{tag}{rst} on {byellow}{address}{rst} returned non-zero exit code: {process.returncode}') - async with target.lock: - with open(os.path.join(scandir, '_errors.log'), 'a') as file: - file.writelines(e('[*] Port scan {tag} returned non-zero exit code: {process.returncode}. Command: {command}\n')) - return {'returncode': process.returncode} - else: - info('Port scan {bgreen}{tag}{rst} on {byellow}{address}{rst} finished successfully in {elapsed_time}') - - ports = results[0] - if len(ports) == 0: - return {'returncode': -1} - - ports = ','.join(ports) - - command = e(service_detection[0]) - pattern = service_detection[1] - - info('Running service detection {bgreen}{tag}{rst} on {byellow}{address}{rst}' + (' with {bblue}{command}{rst}' if verbose >= 1 else '')) - - async with target.lock: - with open(os.path.join(scandir, '_commands.log'), 'a') as file: - file.writelines(e('{command}\n\n')) - - start_time = time.time() - process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') - async with target.lock: - target.running_tasks.append(tag) - - output = [ - parse_service_detection(process.stdout, tag, target, pattern), - read_stream(process.stderr, target, tag=tag, color=Fore.RED) - ] - - results = await asyncio.gather(*output) - - await process.wait() - async with target.lock: - target.running_tasks.remove(tag) - elapsed_time = calculate_elapsed_time(start_time) - - if process.returncode != 0: - error('Service detection {bred}{tag}{rst} on {byellow}{address}{rst} returned non-zero exit code: {process.returncode}') - async with target.lock: - with open(os.path.join(scandir, '_errors.log'), 'a') as file: - file.writelines(e('[*] Service detection {tag} returned non-zero exit code: {process.returncode}. Command: {command}\n')) - else: - info('Service detection {bgreen}{tag}{rst} on {byellow}{address}{rst} finished successfully in {elapsed_time}') - - services = results[0] - - return {'returncode': process.returncode, 'name': 'run_portscan', 'services': services} - -async def start_heartbeat(target, period=60): - while True: - await asyncio.sleep(period) - async with target.lock: - tasks = target.running_tasks - count = len(tasks) - - tasks_list = '' - if verbose >= 1: - tasks_list = ': {bgreen}' + ', '.join(tasks) + '{rst}' - - current_time = datetime.now().strftime('%H:%M:%S') - - if count > 1: - info('{bgreen}[{current_time}]{rst} - There are {byellow}{count}{rst} tasks still running on {byellow}{target.address}{rst}' + tasks_list) - elif count == 1: - info('{bgreen}[{current_time}]{rst} - There is {byellow}1{rst} task still running on {byellow}{target.address}{rst}' + tasks_list) - -async def scan_services(loop, semaphore, target): - address = target.address - scandir = target.scandir - pending = [] - - heartbeat = loop.create_task(start_heartbeat(target, period=heartbeat_interval)) - - for profile in port_scan_profiles_config: - if profile == port_scan_profile: - for scan in port_scan_profiles_config[profile]: - service_detection = (port_scan_profiles_config[profile][scan]['service-detection']['command'], port_scan_profiles_config[profile][scan]['service-detection']['pattern']) - if 'port-scan' in port_scan_profiles_config[profile][scan]: - port_scan = (port_scan_profiles_config[profile][scan]['port-scan']['command'], port_scan_profiles_config[profile][scan]['port-scan']['pattern']) - pending.append(run_portscan(semaphore, scan, target, service_detection, port_scan)) - else: - pending.append(run_portscan(semaphore, scan, target, service_detection)) - break - - services = [] - - while True: - if not pending: - heartbeat.cancel() - break - - done, pending = await asyncio.wait(pending, return_when=FIRST_COMPLETED) - - for task in done: - result = task.result() - - if result['returncode'] == 0: - if result['name'] == 'run_portscan': - for service_tuple in result['services']: - if service_tuple not in services: - services.append(service_tuple) - else: - continue - - protocol = service_tuple[0] - port = service_tuple[1] - service = service_tuple[2] - - info('Found {bmagenta}{service}{rst} on {bmagenta}{protocol}/{port}{rst} on target {byellow}{address}{rst}') - - if not only_scans_dir: - with open(os.path.join(target.reportdir, 'notes.txt'), 'a') as file: - file.writelines(e('[*] {service} found on {protocol}/{port}.\n\n\n\n')) - - if protocol == 'udp': - nmap_extra = nmap + " -sU" - else: - nmap_extra = nmap - - secure = True if 'ssl' in service or 'tls' in service else False - - # Special cases for HTTP. - scheme = 'https' if 'https' in service or 'ssl' in service or 'tls' in service else 'http' - - if service.startswith('ssl/') or service.startswith('tls/'): - service = service[4:] - - for service_scan in service_scans_config: - # Skip over configurable variables since the python toml parser cannot iterate over tables only. - if service_scan in ['username_wordlist', 'password_wordlist']: - continue - - ignore_service = False - if 'ignore-service-names' in service_scans_config[service_scan]: - for ignore_service_name in service_scans_config[service_scan]['ignore-service-names']: - if re.search(ignore_service_name, service): - ignore_service = True - break - - if ignore_service: - continue - - matched_service = False - - if 'service-names' in service_scans_config[service_scan]: - for service_name in service_scans_config[service_scan]['service-names']: - if re.search(service_name, service): - matched_service = True - break - - if not matched_service: - continue - - if 'manual' in service_scans_config[service_scan]: - heading = False - with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: - for manual in service_scans_config[service_scan]['manual']: - if 'description' in manual: - if not heading: - file.writelines(e('[*] {service} on {protocol}/{port}\n\n')) - heading = True - description = manual['description'] - file.writelines(e('\t[-] {description}\n\n')) - if 'commands' in manual: - if not heading: - file.writelines(e('[*] {service} on {protocol}/{port}\n\n')) - heading = True - for manual_command in manual['commands']: - manual_command = e(manual_command) - file.writelines('\t\t' + e('{manual_command}\n\n')) - if heading: - file.writelines('\n') - - if 'scan' in service_scans_config[service_scan]: - for scan in service_scans_config[service_scan]['scan']: - - if 'name' in scan: - name = scan['name'] - if 'command' in scan: - tag = e('{protocol}/{port}/{name}') - command = scan['command'] - - if 'ports' in scan: - port_match = False - - if protocol == 'tcp': - if 'tcp' in scan['ports']: - for tcp_port in scan['ports']['tcp']: - if port == tcp_port: - port_match = True - break - elif protocol == 'udp': - if 'udp' in scan['ports']: - for udp_port in scan['ports']['udp']: - if port == udp_port: - port_match = True - break - - if port_match == False: - warn(Fore.YELLOW + '[' + Style.BRIGHT + tag + Style.NORMAL + '] Scan cannot be run against {protocol} port {port}. Skipping.' + Fore.RESET) - continue - - if 'run_once' in scan and scan['run_once'] == True: - scan_tuple = (name,) - if scan_tuple in target.scans: - warn(Fore.YELLOW + '[' + Style.BRIGHT + tag + ' on ' + address + Style.NORMAL + '] Scan should only be run once and it appears to have already been queued. Skipping.' + Fore.RESET) - continue - else: - target.scans.append(scan_tuple) - else: - scan_tuple = (protocol, port, service, name) - if scan_tuple in target.scans: - warn(Fore.YELLOW + '[' + Style.BRIGHT + tag + ' on ' + address + Style.NORMAL + '] Scan appears to have already been queued, but it is not marked as run_once in service-scans.toml. Possible duplicate tag? Skipping.' + Fore.RESET) - continue - else: - target.scans.append(scan_tuple) - - patterns = [] - if 'pattern' in scan: - patterns = scan['pattern'] - - pending.add(asyncio.ensure_future(run_cmd(semaphore, e(command), target, tag=tag, patterns=patterns))) - -def scan_host(target, concurrent_scans, outdir): - start_time = time.time() - info('Scanning target {byellow}{target.address}{rst}') - - if single_target: - basedir = os.path.abspath(outdir) - else: - basedir = os.path.abspath(os.path.join(outdir, target.address + srvname)) - target.basedir = basedir - os.makedirs(basedir, exist_ok=True) - - if not only_scans_dir: - exploitdir = os.path.abspath(os.path.join(basedir, 'exploit')) - os.makedirs(exploitdir, exist_ok=True) - - lootdir = os.path.abspath(os.path.join(basedir, 'loot')) - os.makedirs(lootdir, exist_ok=True) - - reportdir = os.path.abspath(os.path.join(basedir, 'report')) - target.reportdir = reportdir - os.makedirs(reportdir, exist_ok=True) - - open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close() - open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close() - - screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots')) - os.makedirs(screenshotdir, exist_ok=True) - - scandir = os.path.abspath(os.path.join(basedir, 'scans')) - target.scandir = scandir - os.makedirs(scandir, exist_ok=True) - - os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True) - - # Use a lock when writing to specific files that may be written to by other asynchronous functions. - target.lock = asyncio.Lock() - - # Get event loop for current process. - loop = asyncio.get_event_loop() - - # Create a semaphore to limit number of concurrent scans. - semaphore = asyncio.Semaphore(concurrent_scans) - - try: - loop.run_until_complete(scan_services(loop, semaphore, target)) - elapsed_time = calculate_elapsed_time(start_time) - info('Finished scanning target {byellow}{target.address}{rst} in {elapsed_time}') - except KeyboardInterrupt: - sys.exit(1) - -class Target: - def __init__(self, address): - self.address = address - self.basedir = '' - self.reportdir = '' - self.scandir = '' - self.scans = [] - self.lock = None - self.running_tasks = [] - - - -def main(): - global single_target - global only_scans_dir - global port_scan_profile - global heartbeat_interval - global nmap - global srvname - global verbose - - _init() - parser = argparse.ArgumentParser(description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') - parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs="*") - parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') - parser.add_argument('-ct', '--concurrent-targets', action='store', metavar='', type=int, default=5, help='The maximum number of target hosts to scan concurrently. Default: %(default)s') - parser.add_argument('-cs', '--concurrent-scans', action='store', metavar='', type=int, default=10, help='The maximum number of scans to perform per target host. Default: %(default)s') - parser.add_argument('--profile', action='store', default='default', dest='profile_name', help='The port scanning profile to use (defined in port-scan-profiles.toml). Default: %(default)s') - parser.add_argument('-o', '--output', action='store', default='results', dest='output_dir', help='The output directory for results. Default: %(default)s') - parser.add_argument('--single-target', action='store_true', default=False, help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: false') - parser.add_argument('--only-scans-dir', action='store_true', default=False, help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: false') - parser.add_argument('--heartbeat', action='store', type=int, default=60, help='Specifies the heartbeat interval (in seconds) for task status messages. Default: %(default)s') - nmap_group = parser.add_mutually_exclusive_group() - nmap_group.add_argument('--nmap', action='store', default='-vv --reason -Pn', help='Override the {nmap_extra} variable in scans. Default: %(default)s') - nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.') - parser.add_argument('-v', '--verbose', action='count', default=0, help='Enable verbose output. Repeat for more verbosity.') - parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running. Default: false') - parser.error = lambda s: fail(s[0].upper() + s[1:]) - args = parser.parse_args() - - single_target = args.single_target - only_scans_dir = args.only_scans_dir - - errors = False - - if args.concurrent_targets <= 0: - error('Argument -ch/--concurrent-targets: must be at least 1.') - errors = True - - concurrent_scans = args.concurrent_scans - - if concurrent_scans <= 0: - error('Argument -ct/--concurrent-scans: must be at least 1.') - errors = True - - port_scan_profile = args.profile_name - - found_scan_profile = False - for profile in port_scan_profiles_config: - if profile == port_scan_profile: - found_scan_profile = True - for scan in port_scan_profiles_config[profile]: - if 'service-detection' not in port_scan_profiles_config[profile][scan]: - error('The {profile}.{scan} scan does not have a defined service-detection section. Every scan must at least have a service-detection section defined with a command and a corresponding pattern that extracts the protocol (TCP/UDP), port, and service from the result.') - errors = True - else: - if 'command' not in port_scan_profiles_config[profile][scan]['service-detection']: - error('The {profile}.{scan}.service-detection section does not have a command defined. Every service-detection section must have a command and a corresponding pattern that extracts the protocol (TCP/UDP), port, and service from the results.') - errors = True - else: - if '{ports}' in port_scan_profiles_config[profile][scan]['service-detection']['command'] and 'port-scan' not in port_scan_profiles_config[profile][scan]: - error('The {profile}.{scan}.service-detection command appears to reference a port list but there is no port-scan section defined in {profile}.{scan}. Define a port-scan section with a command and corresponding pattern that extracts port numbers from the result, or replace the reference with a static list of ports.') - errors = True - - if 'pattern' not in port_scan_profiles_config[profile][scan]['service-detection']: - error('The {profile}.{scan}.service-detection section does not have a pattern defined. Every service-detection section must have a command and a corresponding pattern that extracts the protocol (TCP/UDP), port, and service from the results.') - errors = True - else: - if not all(x in port_scan_profiles_config[profile][scan]['service-detection']['pattern'] for x in ['(?P', '(?P', '(?P']): - error('The {profile}.{scan}.service-detection pattern does not contain one or more of the following matching groups: port, protocol, service. Ensure that all three of these matching groups are defined and capture the relevant data, e.g. (?P\d+)') - errors = True - - if 'port-scan' in port_scan_profiles_config[profile][scan]: - if 'command' not in port_scan_profiles_config[profile][scan]['port-scan']: - error('The {profile}.{scan}.port-scan section does not have a command defined. Every port-scan section must have a command and a corresponding pattern that extracts the port from the results.') - errors = True - - if 'pattern' not in port_scan_profiles_config[profile][scan]['port-scan']: - error('The {profile}.{scan}.port-scan section does not have a pattern defined. Every port-scan section must have a command and a corresponding pattern that extracts the port from the results.') - errors = True - else: - if '(?P' not in port_scan_profiles_config[profile][scan]['port-scan']['pattern']: - error('The {profile}.{scan}.port-scan pattern does not contain a port matching group. Ensure that the port matching group is defined and captures the relevant data, e.g. (?P\d+)') - errors = True - break - - if not found_scan_profile: - error('Argument --profile: must reference a port scan profile defined in {port_scan_profiles_config_file}. No such profile found: {port_scan_profile}') - errors = True - - heartbeat_interval = args.heartbeat - - nmap = args.nmap - if args.nmap_append: - nmap += " " + args.nmap_append - - outdir = args.output_dir - srvname = '' - verbose = args.verbose - - raw_targets = args.targets - targets = [] - - if len(args.target_file) > 0: - if not os.path.isfile(args.target_file): - error('The target file {args.target_file} was not found.') - sys.exit(1) - try: - with open(args.target_file, 'r') as f: - lines = f.read() - for line in lines.splitlines(): - line = line.strip() - if line.startswith('#') or len(line) == 0: continue - if line not in raw_targets: - raw_targets.append(line) - except OSError: - error('The target file {args.target_file} could not be read.') - sys.exit(1) - - for target in raw_targets: - try: - ip = str(ipaddress.ip_address(target)) - - if ip not in targets: - targets.append(ip) - except ValueError: - - try: - target_range = ipaddress.ip_network(target, strict=False) - if not args.disable_sanity_checks and target_range.num_addresses > 256: - error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') - errors = True - else: - for ip in target_range.hosts(): - ip = str(ip) - if ip not in targets: - targets.append(ip) - except ValueError: - - try: - ip = socket.gethostbyname(target) - - if target not in targets: - targets.append(target) - except socket.gaierror: - error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') - errors = True - - if len(targets) == 0: - error('You must specify at least one target to scan!') - errors = True - - if single_target and len(targets) != 1: - error('You cannot provide more than one target when scanning in single-target mode.') - sys.exit(1) - - if not args.disable_sanity_checks and len(targets) > 256: - error('A total of ' + str(len(targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.') - errors = True - - if errors: - sys.exit(1) - - with ProcessPoolExecutor(max_workers=args.concurrent_targets) as executor: - start_time = time.time() - futures = [] - - for address in targets: - target = Target(address) - futures.append(executor.submit(scan_host, target, concurrent_scans, outdir)) - - try: - for future in as_completed(futures): - future.result() - except KeyboardInterrupt: - for future in futures: - future.cancel() - executor.shutdown(wait=False) - sys.exit(1) - - elapsed_time = calculate_elapsed_time(start_time) - info('{bgreen}Finished scanning all targets in {elapsed_time}!{rst}') - - -if __name__ == '__main__': - main() diff --git a/src/autorecon/config/global-patterns-default.toml b/src/autorecon/config/global-patterns-default.toml deleted file mode 100644 index 67f6600..0000000 --- a/src/autorecon/config/global-patterns-default.toml +++ /dev/null @@ -1,8 +0,0 @@ -# Patterns defined in this file will be checked against every line of output (e.g. port scans and service scans) - -[[pattern]] -description = 'Nmap script found a potential vulnerability. ({match})' -pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' - -[[pattern]] -pattern = '(?i)unauthorized' diff --git a/src/autorecon/config/port-scan-profiles-default.toml b/src/autorecon/config/port-scan-profiles-default.toml deleted file mode 100644 index 31cf8ba..0000000 --- a/src/autorecon/config/port-scan-profiles-default.toml +++ /dev/null @@ -1,45 +0,0 @@ -[default] - - [default.nmap-quick] - - [default.nmap-quick.service-detection] - command = 'nmap {nmap_extra} -sV -sC --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - - [default.nmap-full-tcp] - - [default.nmap-full-tcp.service-detection] - command = 'nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - - [default.nmap-top-20-udp] - - [default.nmap-top-20-udp.service-detection] - command = 'nmap {nmap_extra} -sU -A --top-ports=20 --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - -[quick] - - [quick.nmap-quick] - - [quick.nmap-quick.service-detection] - command = 'nmap {nmap_extra} -sV --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - - [quick.nmap-top-20-udp] - - [quick.nmap-top-20-udp.service-detection] - command = 'nmap {nmap_extra} -sU -A --top-ports=20 --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - -[udp] - - [udp.udp-top-20] - - [udp.udp-top-20.port-scan] - command = 'unicornscan -mU -p 631,161,137,123,138,1434,445,135,67,53,139,500,68,520,1900,4500,514,49152,162,69 {address} 2>&1 | tee "{scandir}/_top_20_udp_unicornscan.txt"' - pattern = '^UDP open\s*[\w-]+\[\s*(?P\d+)\].*$' - - [udp.udp-top-20.service-detection] - command = 'nmap {nmap_extra} -sU -A -p {ports} --version-all -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}' - pattern = '^(?P\d+)\/(?P(udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' diff --git a/src/autorecon/config/service-scans-default.toml b/src/autorecon/config/service-scans-default.toml deleted file mode 100644 index 55197e4..0000000 --- a/src/autorecon/config/service-scans-default.toml +++ /dev/null @@ -1,586 +0,0 @@ -# Configurable Variables -username_wordlist = '/usr/share/seclists/Usernames/top-usernames-shortlist.txt' -password_wordlist = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' - -[all-services] # Define scans here that you want to run against all services. - -service-names = [ - '.+' -] - - [[all-services.scan]] - name = 'sslscan' - command = 'if [ "{secure}" == "True" ]; then sslscan --show-certificate --no-colour {address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_sslscan.txt"; fi' - -[cassandra] - -service-names = [ - '^apani1' -] - - [[cassandra.scan]] - name = 'nmap-cassandra' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(cassandra* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cassandra_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cassandra_nmap.xml" {address}' - -[cups] - -service-names = [ - '^ipp' -] - - [[cups.scan]] - name = 'nmap-cups' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(cups* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cups_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cups_nmap.xml" {address}' - -[distcc] - -service-names = [ - '^distccd' -] - - [[distcc.scan]] - name = 'nmap-distcc' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,distcc-cve2004-2687" --script-args="distcc-cve2004-2687.cmd=id" -oN "{scandir}/{protocol}_{port}_distcc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_distcc_nmap.xml" {address}' - -[dns] - -service-names = [ - '^domain' -] - - [[dns.scan]] - name = 'nmap-dns' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}' - -[finger] - -service-names = [ - '^finger' -] - - [[finger.scan]] - name = 'nmap-finger' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,finger" -oN "{scandir}/{protocol}_{port}_finger_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_finger_nmap.xml" {address}' - -[ftp] - -service-names = [ - '^ftp', - '^ftp\-data' -] - - [[ftp.scan]] - name = 'nmap-ftp' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}' - - [[ftp.scan.pattern]] - description = 'Anonymous FTP Enabled!' - pattern = 'Anonymous FTP login allowed' - - [[ftp.manual]] - description = 'Bruteforce logins:' - commands = [ - 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', - 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' - ] - -[http] - -service-names = [ - '^http', -] - -ignore-service-names = [ - '^nacn_http$' -] - - [[http.scan]] - name = 'nmap-http' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(http* or ssl*) and not (brute or broadcast or dos or external or http-slowloris* or fuzzer)" -oN "{scandir}/{protocol}_{port}_http_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_{scheme}_nmap.xml" {address}' - - [[http.scan.pattern]] - description = 'Identified HTTP Server: {match}' - pattern = 'Server: ([^\n]+)' - - [[http.scan.pattern]] - description = 'WebDAV is enabled' - pattern = 'WebDAV is ENABLED' - - [[http.scan]] - name = 'curl-index' - command = 'curl -sSik {scheme}://{address}:{port}/ -m 10 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_index.html"' - - [[http.scan.pattern]] - pattern = '(?i)Powered by [^\n]+' - - [[http.scan]] - name = 'curl-robots' - command = 'curl -sSik {scheme}://{address}:{port}/robots.txt -m 10 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_robots.txt"' - - [[http.scan]] - name = 'wkhtmltoimage' - command = 'if hash wkhtmltoimage 2> /dev/null; then wkhtmltoimage --format png {scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{scheme}_screenshot.png; fi' - - [[http.scan]] - name = 'whatweb' - command = 'whatweb --color=never --no-errors -a 3 -v {scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_whatweb.txt"' - - [[http.scan]] - name = 'feroxbuster' - command = 'feroxbuster -u {scheme}://{address}:{port} -t 10 -w /usr/share/seclists/Discovery/Web-Content/common.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{scheme}_feroxbuster.txt' - - [[http.manual]] - description = '(nikto) old but generally reliable web server enumeration tool' - commands = [ - 'nikto -ask=no -h {scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_nikto.txt"' - ] - - [[http.manual]] - description = '(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:' - commands = [ - 'feroxbuster -u {scheme}://{address}:{port} -t 10 -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{scheme}_feroxbuster_big.txt', - 'feroxbuster -u {scheme}://{address}:{port} -t 10 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{scheme}_feroxbuster_dirbuster.txt' - ] - - [[http.manual]] - description = '(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:' - commands = [ - 'dirsearch -u {scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --plain-text-report="{scandir}/{protocol}_{port}_{scheme}_dirsearch_big.txt"', - 'dirsearch -u {scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --plain-text-report="{scandir}/{protocol}_{port}_{scheme}_dirsearch_dirbuster.txt"' - ] - - [[http.manual]] - description = '(dirb) Recursive directory/file enumeration for web servers using various wordlists (same as dirsearch above):' - commands = [ - 'dirb {scheme}://{address}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{scheme}_dirb_big.txt"', - 'dirb {scheme}://{address}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{scheme}_dirb_dirbuster.txt"' - ] - - [[http.manual]] - description = '(gobuster v3) Directory/file enumeration for web servers using various wordlists (same as dirb above):' - commands = [ - 'gobuster dir -u {scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_big.txt"', - 'gobuster dir -u {scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_dirbuster.txt"' - ] - - [[http.manual]] - description = '(gobuster v1 & v2) Directory/file enumeration for web servers using various wordlists (same as dirb above):' - commands = [ - 'gobuster -u {scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_big.txt"', - 'gobuster -u {scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{scheme}_gobuster_dirbuster.txt"' - ] - - [[http.manual]] - description = '(wpscan) WordPress Security Scanner (useful if WordPress is found):' - commands = [ - 'wpscan --url {scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{scheme}_wpscan.txt"' - ] - - [[http.manual]] - description = "Credential bruteforcing commands (don't run these without modifying them):" - commands = [ - 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{scheme}_auth_hydra.txt" {scheme}-get://{address}/path/to/auth/area', - 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', - 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{scheme}_form_hydra.txt" {scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', - 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"', - ] - -[imap] - -service-names = [ - '^imap' -] - - [[imap.scan]] - name = 'nmap-imap' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}' - -[kerberos] - -service-names = [ - '^kerberos', - '^kpasswd' -] - - [[kerberos.scan]] - name = 'nmap-kerberos' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}' - -[ldap] - -service-names = [ - '^ldap' -] - - [[ldap.scan]] - name = 'nmap-ldap' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(ldap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ldap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ldap_nmap.xml" {address}' - - [[ldap.scan]] - name = 'enum4linux' - command = 'enum4linux -a -M -l -d {address} 2>&1 | tee "{scandir}/enum4linux.txt"' - run_once = true - ports.tcp = [139, 389, 445] - ports.udp = [137] - - [[ldap.manual]] - description = 'ldapsearch command (modify before running)' - commands = [ - 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' - ] - -[mongodb] - -service-names = [ - '^mongod' -] - - [[mongodb.scan]] - name = 'nmap-mongodb' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(mongodb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mongodb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mongodb_nmap.xml" {address}' - -[mssql] - -service-names = [ - '^mssql', - '^ms\-sql' -] - - [[mssql.scan]] - name = 'nmap-mssql' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}' - - [[mssql.manual]] - description = '(sqsh) interactive database shell' - commands = [ - 'sqsh -U -P -S {address}:{port}' - ] - -[mysql] - -service-names = [ - '^mysql' -] - - [[mysql.scan]] - name = 'nmap-mysql' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}' - -[nfs] - -service-names = [ - '^nfs', - '^rpcbind' -] - - [[nfs.scan]] - name = 'nmap-nfs' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(rpcinfo or nfs*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_nfs_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nfs_nmap.xml" {address}' - - [[nfs.scan]] - name = 'showmount' - command = 'showmount -e {address} 2>&1 | tee "{scandir}/{protocol}_{port}_showmount.txt"' - -[nntp] - -service-names = [ - '^nntp' -] - - [[nntp.scan]] - name = 'nmap-nntp' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,nntp-ntlm-info" -oN "{scandir}/{protocol}_{port}_nntp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nntp_nmap.xml" {address}' - -[oracle] - -service-names = [ - '^oracle' -] - - [[oracle.scan]] - name = 'nmap-oracle' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}' - - [[oracle.scan]] - name = 'oracle-tnscmd-ping' - command = 'tnscmd10g ping -h {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_tnscmd_ping.txt"' - - [[oracle.scan]] - name = 'oracle-tnscmd-version' - command = 'tnscmd10g version -h {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_tnscmd_version.txt"' - - [[oracle.scan]] - name = 'oracle-tnscmd-version' - command = 'tnscmd10g version -h {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_tnscmd_version.txt"' - - [[oracle.scan]] - name = 'oracle-scanner' - command = 'oscanner -v -s {address} -P {port} 2>&1 | tee "{scandir}/{protocol}_{port}_oracle_scanner.txt"' - - [[oracle.manual]] - description = 'Brute-force SIDs using Nmap' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}' - - [[oracle.manual]] - description = 'Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:' - commands = [ - 'python odat.py tnscmd -s {address} -p {port} --ping', - 'python odat.py tnscmd -s {address} -p {port} --version', - 'python odat.py tnscmd -s {address} -p {port} --status', - 'python odat.py sidguesser -s {address} -p {port}', - 'python odat.py passwordguesser -s {address} -p {port} -d --accounts-file accounts/accounts_multiple.txt', - 'python odat.py tnspoison -s {address} -p {port} -d --test-module' - ] - - [[oracle.manual]] - description = 'Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:' - commands = [ - 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000' - ] - -[pop3] - -service-names = [ - '^pop3' -] - - [[pop3.scan]] - name = 'nmap-pop3' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(pop3* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_pop3_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_pop3_nmap.xml" {address}' - -[rdp] - -service-names = [ - '^rdp', - '^ms\-wbt\-server', - '^ms\-term\-serv' -] - - [[rdp.scan]] - name = 'nmap-rdp' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(rdp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rdp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rdp_nmap.xml" {address}' - - [[rdp.manual]] - description = 'Bruteforce logins:' - commands = [ - 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', - 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' - ] - -[rmi] - -service-names = [ - '^java\-rmi', - '^rmiregistry' -] - - [[rmi.scan]] - name = 'nmap-rmi' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}' - -[rpc] - -service-names = [ - '^msrpc', - '^rpcbind', - '^erpc' -] - - [[rpc.scan]] - name = 'nmap-msrpc' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,msrpc-enum,rpc-grind,rpcinfo" -oN "{scandir}/{protocol}_{port}_rpc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rpc_nmap.xml" {address}' - - [[rpc.manual]] - description = 'RPC Client:' - commands = [ - 'rpcclient -p {port} -U "" {address}' - ] - -[sip] - -service-names = [ - '^asterisk' -] - - [[sip.scan]] - name = 'nmap-sip' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,sip-enum-users,sip-methods" -oN "{scandir}/{protocol}_{port}_sip_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_sip_nmap.xml" {address}' - - [[sip.scan]] - name = 'svwar' - command = 'svwar -D -m INVITE -p {port} {address}' - -[ssh] - -service-names = [ - '^ssh' -] - - [[ssh.scan]] - name = 'nmap-ssh' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,ssh2-enum-algos,ssh-hostkey,ssh-auth-methods" -oN "{scandir}/{protocol}_{port}_ssh_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ssh_nmap.xml" {address}' - - [[ssh.manual]] - description = 'Bruteforce logins:' - commands = [ - 'hydra -L "{username_wordlist}" -P "{password_wordlist}" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', - 'medusa -U "{username_wordlist}" -P "{password_wordlist}" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' - ] -[smb] - -service-names = [ - '^smb', - '^microsoft\-ds', - '^netbios' -] - - [[smb.scan]] - name = 'nmap-smb' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}' - - [[smb.scan]] - name = 'enum4linux' - command = 'enum4linux -a -M -l -d {address} 2>&1 | tee "{scandir}/enum4linux.txt"' - run_once = true - ports.tcp = [139, 389, 445] - ports.udp = [137] - - [[smb.scan]] - name = 'nbtscan' - command = 'nbtscan -rvh {address} 2>&1 | tee "{scandir}/nbtscan.txt"' - run_once = true - ports.udp = [137] - - [[smb.scan]] - name = 'smbclient' - command = 'smbclient -L\\ -N -I {address} 2>&1 | tee "{scandir}/smbclient.txt"' - run_once = true - ports.tcp = [139, 445] - - [[smb.scan]] - name = 'smbmap-share-permissions' - command = 'smbmap -H {address} -P {port} 2>&1 | tee -a "{scandir}/smbmap-share-permissions.txt"; smbmap -u null -p "" -H {address} -P {port} 2>&1 | tee -a "{scandir}/smbmap-share-permissions.txt"' - - [[smb.scan]] - name = 'smbmap-list-contents' - command = 'smbmap -H {address} -P {port} -R 2>&1 | tee -a "{scandir}/smbmap-list-contents.txt"; smbmap -u null -p "" -H {address} -P {port} -R 2>&1 | tee -a "{scandir}/smbmap-list-contents.txt"' - - [[smb.scan]] - name = 'smbmap-execute-command' - command = 'smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1 | tee -a "{scandir}/smbmap-execute-command.txt"; smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1 | tee -a "{scandir}/smbmap-execute-command.txt"' - - [[smb.manual]] - description = 'Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:' - commands = [ - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' - ] - -[smtp] - -service-names = [ - '^smtp' -] - - [[smtp.scan]] - name = 'nmap-smtp' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}' - - [[smtp.scan]] - name = 'smtp-user-enum' - command = 'smtp-user-enum -M VRFY -U "{username_wordlist}" -t {address} -p {port} 2>&1 | tee "{scandir}/{protocol}_{port}_smtp_user-enum.txt"' - -[snmp] - -service-names = [ - '^snmp' -] - - [[snmp.scan]] - name = 'nmap-snmp' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(snmp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_snmp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_snmp_nmap.xml" {address}' - - [[snmp.scan]] - name = 'onesixtyone' - command = 'onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt -dd {address} 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_onesixtyone.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk' - command = 'snmpwalk -c public -v 1 {address} 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk-system-processes' - command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.1.6.0 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_system_processes.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk-running-processes' - command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.2 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_running_processes.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk-process-paths' - command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.4 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_process_paths.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk-storage-units' - command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_storage_units.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk-software-names' - command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.6.3.1.2 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_software_names.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk-user-accounts' - command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.4.1.77.1.2.25 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_user_accounts.txt"' - run_once = true - ports.udp = [161] - - [[snmp.scan]] - name = 'snmpwalk-tcp-ports' - command = 'snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.6.13.1.3 2>&1 | tee "{scandir}/{protocol}_{port}_snmp_snmpwalk_tcp_ports.txt"' - run_once = true - ports.udp = [161] - -[telnet] - -service-names = [ - '^telnet' -] - - [[telnet.scan]] - name = 'nmap-telnet' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,telnet-encryption,telnet-ntlm-info" -oN "{scandir}/{protocol}_{port}_telnet-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_telnet_nmap.xml" {address}' - -[tftp] - -service-names = [ - '^tftp' -] - - [[tftp.scan]] - name = 'nmap-tftp' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,tftp-enum" -oN "{scandir}/{protocol}_{port}_tftp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_tftp_nmap.xml" {address}' - -[vnc] - -service-names = [ - '^vnc' -] - - [[vnc.scan]] - name = 'nmap-vnc' - command = 'nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}' From 7dfe30abc6d69e302106709bab8caa85af43b74e Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 31 Jul 2021 21:11:57 -0400 Subject: [PATCH 02/95] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b67a694..275286d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ > It's like bowling with bumpers. - [@ippsec](https://twitter.com/ippsec) +# Please Read Before Using + +**This is a public beta of AutoRecon version 2, which is effectively a complete rewrite of version 1. As such, there are no promises about stability, and you should expect bugs. During this beta, testers are encouraged to try out the new features, especially the new plugin functionality, and report bugs when they are found. Feedback on improvements and changes is also encouraged. There is no guarantee that the current plugin system "API" will be the same when version 2 is released.** + +**A wiki will be added to this repository to more fully explain the features in AutoRecon version 2.** + # AutoRecon AutoRecon is a multi-threaded network reconnaissance tool which performs automated enumeration of services. It is intended as a time-saving tool for use in CTFs and other penetration testing environments (e.g. OSCP). It may also be useful in real-world engagements. From f50a09711080701b392cfadafa0a874bdadff04e Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 1 Aug 2021 13:38:19 -0400 Subject: [PATCH 03/95] Update README.md --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 275286d..4652c01 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,20 @@ $ sudo apt install seclists curl enum4linux feroxbuster nbtscan nikto nmap onesi Ensure you have all of the requirements installed as per the previous section. -First install the dependencies: +Clone the repository and switch to the beta branch: + +```bash +$ git clone --branch beta https://github.com/Tib3rius/AutoRecon +``` + +If you already had a copy of the repository, you can run the following from the main directory to get the beta code: + +```bash +$ git pull +$ git checkout beta +``` + +From within the AutoRecon directory, install the dependencies: ```bash $ python3 -m pip install -r requirements.txt From b34cae3aa7d34928426c55588a2cbb46ea5331c8 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 1 Aug 2021 16:55:36 -0400 Subject: [PATCH 04/95] Update autorecon.py Replaced bool variable with boolean to avoid type confusion. --- autorecon.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/autorecon.py b/autorecon.py index 6993359..446f277 100644 --- a/autorecon.py +++ b/autorecon.py @@ -297,8 +297,8 @@ class ServiceScan(Plugin): self.ignore_ports = {'tcp':[], 'udp':[]} self.services = [] self.ignore_services = [] - self.run_once_bool = False - self.require_ssl_bool = False + self.run_once_boolean = False + self.require_ssl_boolean = False @final def add_port_match(self, protocol, port, negative_match=False): @@ -339,12 +339,12 @@ class ServiceScan(Plugin): sys.exit(1) @final - def require_ssl(self, bool): - self.require_ssl_bool = bool + def require_ssl(self, boolean): + self.require_ssl_boolean = boolean @final - def run_once(self, bool): - self.run_once_bool = bool + def run_once(self, boolean): + self.run_once_boolean = boolean class AutoRecon(object): @@ -944,13 +944,13 @@ async def scan_target(target): break if plugin_is_runnable and matching_tags and not excluded_tags: - # Skip plugin if run_once_bool and plugin already in target scans - if plugin.run_once_bool and (plugin.slug,) in target.scans: + # Skip plugin if run_once_boolean and plugin already in target scans + if plugin.run_once_boolean and (plugin.slug,) in target.scans: warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') continue - # Skip plugin if require_ssl_bool and port is not secure - if plugin.require_ssl_bool and not service.secure: + # Skip plugin if require_ssl_boolean and port is not secure + if plugin.require_ssl_boolean and not service.secure: continue # Skip plugin if service port is in ignore_ports: @@ -971,7 +971,7 @@ async def scan_target(target): # TODO: check if plugin matches tags, BUT run manual commands anyway! matching_plugins.append(plugin) - if plugin.manual_commands and (not plugin.run_once_bool or (plugin.run_once_bool and (plugin.slug,) not in target.scans)): + if plugin.manual_commands and (not plugin.run_once_boolean or (plugin.run_once_boolean and (plugin.slug,) not in target.scans)): with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: if not heading: file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) @@ -988,7 +988,7 @@ async def scan_target(target): plugin_tag = service.tag() + '/' + plugin.slug scan_tuple = (service.protocol, service.port, service.name, plugin.slug) - if plugin.run_once_bool: + if plugin.run_once_boolean: scan_tuple = (plugin.slug,) if scan_tuple in target.scans: From 3b1120db1cccaf371dc842bca2d0a57dae23d3b2 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 1 Aug 2021 17:32:28 -0400 Subject: [PATCH 05/95] Update autorecon.py Removed several instances of commented out code. Added exception handling for process killing code. --- autorecon.py | 41 +++++------------------------------------ 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/autorecon.py b/autorecon.py index 446f277..2e0e18d 100644 --- a/autorecon.py +++ b/autorecon.py @@ -124,7 +124,6 @@ class Service: if target.autorecon.config['verbose'] >= 1: info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) - #info('{blue}[{bright}' + address + ' ' + tag + '{srst}]{rst} Running command: ' + cmd) if outfile is not None: outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) @@ -384,15 +383,6 @@ class AutoRecon(object): def add_argument(self, plugin, name, **kwargs): # TODO: make sure name is simple. name = '--' + plugin.slug + '.' + slugify(name) - '''if 'action' in kwargs.keys() and kwargs['action'] != 'store': - if kwargs['action'] in ['store_true'] - else: - if 'metavar' not in kwargs.keys(): - kwargs['metavar'] = 'VALUE' - - if 'metavar' not in kwargs.keys() and 'choices' not in kwargs.keys() and ('action' in kwargs.keys() and kwargs['action'] not in ['store_true', 'store_false']): - kwargs['metavar'] = 'VALUE' - ''' if self.argparse_group is None: self.argparse_group = self.argparse.add_argument_group('plugin arguments', description='These are optional arguments for certain plugins.') @@ -639,7 +629,10 @@ def cancel_all_tasks(signal, frame): for target in autorecon.scanning_targets: for process_list in target.running_tasks.values(): for process_dict in process_list['processes']: - process_dict['process'].kill() + try: + process_dict['process'].kill() + except ProcessLookupError: # Will get raised if the process finishes before we get to killing it. + pass async def start_heartbeat(target, period=60): while True: @@ -712,24 +705,12 @@ async def service_scan(plugin, service): while True: if semaphore.locked(): if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans - # port_scan_task_count = 0 - # for t in asyncio.all_tasks(): - # frame = inspect.getframeinfo(t.get_stack(limit=1)[0]) - # if frame.function == 'port_scan' and 'await plugin.run(target)' in frame.code_context[0]: - # #print(frame) - # port_scan_task_count += 1 - # print("Old Port Scan Task Count: " + str(port_scan_task_count)) port_scan_task_count = 0 for targ in service.target.autorecon.scanning_targets: for process_list in targ.running_tasks.values(): - #print('Length: ' + str(len(process_list))) - #for process_dict in process_list['processes']: if issubclass(process_list['plugin'].__class__, PortScan): - #for process_dict in process_list['processes']: - # print(str(process_dict['process'].returncode) + ': ' + process_dict['cmd']) port_scan_task_count += 1 - #print("New Port Scan Task Count: " + str(new_port_scan_task_count)) if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. if service.target.autorecon.port_scan_semaphore.locked(): @@ -892,8 +873,6 @@ async def scan_target(target): else: continue - #plugin = task.result()['plugin'] - #info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){srst}]{rst} found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') service.target = target @@ -1116,7 +1095,7 @@ async def main(): for key, val in global_toml.items(): if key == 'global' and isinstance(val, dict): # Process global plugin options. for gkey, gvals in global_toml['global'].items(): - if isinstance(gvals, dict):# and 'help' in gvals: + if isinstance(gvals, dict): options = {'metavar':'VALUE'} if 'default' in gvals: @@ -1360,8 +1339,6 @@ async def main(): start_time = time.time() - #sys.exit(0) - pending = [] i = 0 while autorecon.pending_targets: @@ -1385,19 +1362,11 @@ async def main(): if autorecon.pending_targets: pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) - #port_scan_task_count = 0 - #for t in asyncio.all_tasks(): - # if inspect.getframeinfo(t.get_stack(limit=1)[0]).function == 'port_scan': - # port_scan_task_count += 1 - #print("Old Port Scan Task Count: " + str(port_scan_task_count)) - port_scan_task_count = 0 for targ in autorecon.scanning_targets: for process_list in targ.running_tasks.values(): if issubclass(process_list['plugin'].__class__, PortScan): - #print(process_list) port_scan_task_count += 1 - #print("New Port Scan Task Count: " + str(port_scan_task_count)) num_new_targets = math.ceil((autorecon.config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count) if num_new_targets > 0: From 5a584a1b713da8dc3a9303bbde9071b25cfa2a33 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 2 Aug 2021 19:13:09 -0400 Subject: [PATCH 06/95] Reformatting & API Function Name Updates Switched spaces to tabs throughout for consistency. Changed add_port_match() to match_port() Changed add_service_match() to match_service_name() Removed un-used variables. --- LICENSE | 184 +-- README.md | 6 +- autorecon.py | 2302 +++++++++++++++++----------------- plugins/databases.py | 156 +-- plugins/default-port-scan.py | 64 +- plugins/dns.py | 16 +- plugins/ftp.py | 38 +- plugins/http.py | 266 ++-- plugins/kerberos.py | 16 +- plugins/ldap.py | 36 +- plugins/misc.py | 208 +-- plugins/nfs.py | 32 +- plugins/rdp.py | 38 +- plugins/rpc.py | 32 +- plugins/sip.py | 32 +- plugins/smb.py | 116 +- plugins/snmp.py | 72 +- plugins/ssh.py | 38 +- plugins/sslscan.py | 20 +- 19 files changed, 1835 insertions(+), 1837 deletions(-) diff --git a/LICENSE b/LICENSE index f288702..1acb731 100644 --- a/LICENSE +++ b/LICENSE @@ -1,11 +1,11 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble + Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. @@ -68,7 +68,7 @@ patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. - TERMS AND CONDITIONS + TERMS AND CONDITIONS 0. Definitions. @@ -211,26 +211,26 @@ and you may offer support or warranty protection for a fee. produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, @@ -249,46 +249,46 @@ of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be @@ -362,28 +362,28 @@ for which you have or can give appropriate copyright permission. add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you @@ -618,9 +618,9 @@ an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. - END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS - How to Apply These Terms to Your New Programs + How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it @@ -631,31 +631,31 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + + Copyright (C) - This program is free software: you can redistribute it and/or modify - it under the terms of 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands diff --git a/README.md b/README.md index 4652c01..9945c62 100644 --- a/README.md +++ b/README.md @@ -129,9 +129,9 @@ By default, results will be stored in the ./results directory. A new sub directo │   ├── proof.txt │   └── screenshots/ └── scans/ - ├── _commands.log - ├── _manual_commands.txt - └── xml/ + ├── _commands.log + ├── _manual_commands.txt + └── xml/ ``` The exploit directory is intended to contain any exploit code you download / write for the target. diff --git a/autorecon.py b/autorecon.py index 2e0e18d..49950d1 100644 --- a/autorecon.py +++ b/autorecon.py @@ -9,1390 +9,1388 @@ colorama.init() class Pattern: - def __init__(self, pattern, description=None): - self.pattern = pattern - self.description = description + def __init__(self, pattern, description=None): + self.pattern = pattern + self.description = description class Target: - def __init__(self, address, autorecon): - self.address = address - self.autorecon = autorecon - self.basedir = '' - self.reportdir = '' - self.scandir = '' - self.lock = asyncio.Lock() - self.pending_services = [] - self.services = [] - self.scans = [] - self.running_tasks = {} + def __init__(self, address, autorecon): + self.address = address + self.autorecon = autorecon + self.basedir = '' + self.reportdir = '' + self.scandir = '' + self.lock = asyncio.Lock() + self.pending_services = [] + self.services = [] + self.scans = [] + self.running_tasks = {} - async def add_service(self, protocol, port, name, secure=False): - async with self.lock: - self.pending_services.append(Service(protocol, port, name, secure)) + async def add_service(self, protocol, port, name, secure=False): + async with self.lock: + self.pending_services.append(Service(protocol, port, name, secure)) - def extract_service(self, line, regex=None): - return self.autorecon.extract_service(line, regex) + def extract_service(self, line, regex=None): + return self.autorecon.extract_service(line, regex) - async def extract_services(self, stream, regex=None): - return await self.autorecon.extract_services(stream, regex) + async def extract_services(self, stream, regex=None): + return await self.autorecon.extract_services(stream, regex) - async def execute(self, cmd, blocking=True, outfile=None, errfile=None): - target = self + async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + target = self - # Create variables for command references. - address = target.address - scandir = target.scandir + # Create variables for command references. + address = target.address + scandir = target.scandir - nmap_extra = self.autorecon.args.nmap - if self.autorecon.args.nmap_append: - nmap_extra += ' ' + self.autorecon.args.nmap_append + nmap_extra = self.autorecon.args.nmap + if self.autorecon.args.nmap_append: + nmap_extra += ' ' + self.autorecon.args.nmap_append - plugin = inspect.currentframe().f_back.f_locals['self'] + plugin = inspect.currentframe().f_back.f_locals['self'] - cmd = e(cmd) + cmd = e(cmd) - tag = plugin.slug + tag = plugin.slug - if target.autorecon.config['verbose'] >= 1: - info('Port scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + if target.autorecon.config['verbose'] >= 1: + info('Port scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) - if outfile is not None: - outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) + if outfile is not None: + outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) - if errfile is not None: - errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) + if errfile is not None: + errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) - async with target.lock: - with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: - file.writelines(cmd + '\n\n') + async with target.lock: + with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: + file.writelines(cmd + '\n\n') - process, stdout, stderr = await self.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) + process, stdout, stderr = await self.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) - target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) + target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) - if blocking: - while (not (stdout.ended and stderr.ended)): - await asyncio.sleep(0.1) - await process.wait() + if blocking: + while (not (stdout.ended and stderr.ended)): + await asyncio.sleep(0.1) + await process.wait() - return process, stdout, stderr + return process, stdout, stderr class Service: - def __init__(self, protocol, port, name, secure=False): - self.target = None - self.protocol = protocol.lower() - self.port = int(port) - self.name = name - self.secure = secure + def __init__(self, protocol, port, name, secure=False): + self.target = None + self.protocol = protocol.lower() + self.port = int(port) + self.name = name + self.secure = secure - @final - def tag(self): - return self.protocol + '/' + str(self.port) + '/' + self.name + @final + def tag(self): + return self.protocol + '/' + str(self.port) + '/' + self.name - @final - def full_tag(self): - return self.protocol + '/' + str(self.port) + '/' + self.name + '/' + ('secure' if self.secure else 'insecure') + @final + def full_tag(self): + return self.protocol + '/' + str(self.port) + '/' + self.name + '/' + ('secure' if self.secure else 'insecure') - @final - async def execute(self, cmd, blocking=True, outfile=None, errfile=None): - target = self.target + @final + async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + target = self.target - # Create variables for command references. - address = target.address - scandir = target.scandir - protocol = self.protocol - port = self.port - name = self.name + # Create variables for command references. + address = target.address + scandir = target.scandir + protocol = self.protocol + port = self.port + name = self.name - # Special cases for HTTP. - http_scheme = 'https' if 'https' in self.name or self.secure is True else 'http' + # Special cases for HTTP. + http_scheme = 'https' if 'https' in self.name or self.secure is True else 'http' - nmap_extra = self.target.autorecon.args.nmap - if self.target.autorecon.args.nmap_append: - nmap_extra += ' ' + self.target.autorecon.args.nmap_append + nmap_extra = self.target.autorecon.args.nmap + if self.target.autorecon.args.nmap_append: + nmap_extra += ' ' + self.target.autorecon.args.nmap_append - if protocol == 'udp': - nmap_extra += ' -sU' + if protocol == 'udp': + nmap_extra += ' -sU' - plugin = inspect.currentframe().f_back.f_locals['self'] + plugin = inspect.currentframe().f_back.f_locals['self'] - cmd = e(cmd) + cmd = e(cmd) - tag = self.tag() + '/' + plugin.slug + tag = self.tag() + '/' + plugin.slug - if target.autorecon.config['verbose'] >= 1: - info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + if target.autorecon.config['verbose'] >= 1: + info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) - if outfile is not None: - outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) + if outfile is not None: + outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) - if errfile is not None: - errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) + if errfile is not None: + errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) - async with target.lock: - with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: - file.writelines(e('{cmd}\n\n')) + async with target.lock: + with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: + file.writelines(e('{cmd}\n\n')) - process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) + process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) - target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) + target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) - if blocking: - while (not (stdout.ended and stderr.ended)): - await asyncio.sleep(0.1) - await process.wait() + if blocking: + while (not (stdout.ended and stderr.ended)): + await asyncio.sleep(0.1) + await process.wait() - return process, stdout, stderr + return process, stdout, stderr class CommandStreamReader(object): - def __init__(self, stream, target, tag,patterns=None, outfile=None): - self.stream = stream - self.target = target - self.tag = tag - self.lines = [] - self.patterns = patterns or [] - self.outfile = outfile - self.ended = False + def __init__(self, stream, target, tag,patterns=None, outfile=None): + self.stream = stream + self.target = target + self.tag = tag + self.lines = [] + self.patterns = patterns or [] + self.outfile = outfile + self.ended = False - async def _read(self): - while True: - if self.stream.at_eof(): - break - line = (await self.stream.readline()).decode('utf8').rstrip() - if self.target.autorecon.config['verbose'] >= 2: - if line != '': - info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} ' + line.replace('{', '{{').replace('}', '}}')) - for p in self.patterns: - matches = p.pattern.findall(line) - for match in matches: - async with self.target.lock: - with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file: - if p.description: - if self.target.autorecon.config['verbose'] >= 1: - info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}' + p.description.replace('{match}', '{bblue}' + match + '{crst}{bmagenta}') + '{rst}') - file.writelines(p.description.replace('{match}', match) + '\n\n') - else: - if self.target.autorecon.config['verbose'] >= 1: - info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}Matched Pattern: {bblue}' + match + '{rst}') - file.writelines('Matched Pattern: ' + match + '\n\n') + async def _read(self): + while True: + if self.stream.at_eof(): + break + line = (await self.stream.readline()).decode('utf8').rstrip() + if self.target.autorecon.config['verbose'] >= 2: + if line != '': + info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} ' + line.replace('{', '{{').replace('}', '}}')) + for p in self.patterns: + matches = p.pattern.findall(line) + for match in matches: + async with self.target.lock: + with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file: + if p.description: + if self.target.autorecon.config['verbose'] >= 1: + info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}' + p.description.replace('{match}', '{bblue}' + match + '{crst}{bmagenta}') + '{rst}') + file.writelines(p.description.replace('{match}', match) + '\n\n') + else: + if self.target.autorecon.config['verbose'] >= 1: + info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}Matched Pattern: {bblue}' + match + '{rst}') + file.writelines('Matched Pattern: ' + match + '\n\n') - if self.outfile is not None: - with open(self.outfile, 'a') as writer: - writer.write(line + '\n') - self.lines.append(line) - self.ended = True + if self.outfile is not None: + with open(self.outfile, 'a') as writer: + writer.write(line + '\n') + self.lines.append(line) + self.ended = True - async def readline(self): - while True: - try: - return self.lines.pop(0) - except IndexError: - if self.ended: - return None - else: - await asyncio.sleep(0.1) + async def readline(self): + while True: + try: + return self.lines.pop(0) + except IndexError: + if self.ended: + return None + else: + await asyncio.sleep(0.1) class Plugin(object): - def __init__(self): - self.name = None - self.slug = None - self.description = None - self.type = None - self.tags = ['default'] - self.priority = 1 - self.patterns = [] - self.match = None - self.manual_commands = {} - self.autorecon = None - self.disabled = False + def __init__(self): + self.name = None + self.slug = None + self.description = None + self.tags = ['default'] + self.priority = 1 + self.patterns = [] + self.manual_commands = {} + self.autorecon = None + self.disabled = False - @final - def add_option(self, name, default=None, help=None): - self.autorecon.add_argument(self, name, metavar='VALUE', default=default, help=help) + @final + def add_option(self, name, default=None, help=None): + self.autorecon.add_argument(self, name, metavar='VALUE', default=default, help=help) - @final - def add_constant_option(self, name, const, default=None, help=None): - self.autorecon.add_argument(self, name, action='store_const', const=const, default=default, help=help) + @final + def add_constant_option(self, name, const, default=None, help=None): + self.autorecon.add_argument(self, name, action='store_const', const=const, default=default, help=help) - @final - def add_true_option(self, name, help=None): - self.autorecon.add_argument(self, name, action='store_true', help=help) + @final + def add_true_option(self, name, help=None): + self.autorecon.add_argument(self, name, action='store_true', help=help) - @final - def add_false_option(self, name, help=None): - self.autorecon.add_argument(self, name, action='store_false', help=help) + @final + def add_false_option(self, name, help=None): + self.autorecon.add_argument(self, name, action='store_false', help=help) - @final - def add_list_option(self, name, default=None, help=None): - self.autorecon.add_argument(self, name, action='append', metavar='VALUE', default=default, help=help) + @final + def add_list_option(self, name, default=None, help=None): + self.autorecon.add_argument(self, name, action='append', metavar='VALUE', default=default, help=help) - @final - def add_choice_option(self, name, choices, default=None, help=None): - if not isinstance(choices, list): - fail('The choices argument for ' + self.name + '\'s ' + name + ' choice option should be a list.') - self.autorecon.add_argument(self, name, choices=choices, default=default, help=help) + @final + def add_choice_option(self, name, choices, default=None, help=None): + if not isinstance(choices, list): + fail('The choices argument for ' + self.name + '\'s ' + name + ' choice option should be a list.') + self.autorecon.add_argument(self, name, choices=choices, default=default, help=help) - @final - def get_option(self, name): - # TODO: make sure name is simple. - name = self.slug.replace('-', '_') + '.' + slugify(name).replace('-', '_') + @final + def get_option(self, name): + # TODO: make sure name is simple. + name = self.slug.replace('-', '_') + '.' + slugify(name).replace('-', '_') - if name in vars(self.autorecon.args): - return vars(self.autorecon.args)[name] - else: - return None + if name in vars(self.autorecon.args): + return vars(self.autorecon.args)[name] + else: + return None - @final - def get_global_option(self, name): - name = 'global.' + slugify(name).replace('-', '_') + @final + def get_global_option(self, name): + name = 'global.' + slugify(name).replace('-', '_') - if name in vars(self.autorecon.args): - return vars(self.autorecon.args)[name] - else: - return None + if name in vars(self.autorecon.args): + return vars(self.autorecon.args)[name] + else: + return None - @final - def get_global(self, name): - return self.get_global_option(name) + @final + def get_global(self, name): + return self.get_global_option(name) - @final - def add_manual_commands(self, description, commands): - if not isinstance(commands, list): - commands = [commands] - self.manual_commands[description] = commands + @final + def add_manual_commands(self, description, commands): + if not isinstance(commands, list): + commands = [commands] + self.manual_commands[description] = commands - @final - def add_manual_command(self, description, command): - self.add_manual_commands(description, command) + @final + def add_manual_command(self, description, command): + self.add_manual_commands(description, command) - @final - def add_pattern(self, pattern, description=None): - try: - compiled = re.compile(pattern) - if description: - self.patterns.append(Pattern(compiled, description=description)) - else: - self.patterns.append(Pattern(compiled)) - except re.error: - fail('Error: The pattern "' + pattern + '" in the plugin "' + self.name + '" is invalid regex.') + @final + def add_pattern(self, pattern, description=None): + try: + compiled = re.compile(pattern) + if description: + self.patterns.append(Pattern(compiled, description=description)) + else: + self.patterns.append(Pattern(compiled)) + except re.error: + fail('Error: The pattern "' + pattern + '" in the plugin "' + self.name + '" is invalid regex.') class PortScan(Plugin): - def __init__(self): - super().__init__() + def __init__(self): + super().__init__() - async def run(self, target): - raise NotImplementedError + async def run(self, target): + raise NotImplementedError class ServiceScan(Plugin): - def __init__(self): - super().__init__() - self.ports = {'tcp':[], 'udp':[]} - self.ignore_ports = {'tcp':[], 'udp':[]} - self.services = [] - self.ignore_services = [] - self.run_once_boolean = False - self.require_ssl_boolean = False + def __init__(self): + super().__init__() + self.ports = {'tcp':[], 'udp':[]} + self.ignore_ports = {'tcp':[], 'udp':[]} + self.service_names = [] + self.ignore_service_names = [] + self.run_once_boolean = False + self.require_ssl_boolean = False - @final - def add_port_match(self, protocol, port, negative_match=False): - protocol = protocol.lower() - if protocol not in ['tcp', 'udp']: - print('Invalid protocol.') - sys.exit(1) - else: - if not isinstance(port, list): - port = [port] + @final + def match_port(self, protocol, port, negative_match=False): + protocol = protocol.lower() + if protocol not in ['tcp', 'udp']: + print('Invalid protocol.') + sys.exit(1) + else: + if not isinstance(port, list): + port = [port] - port = list(map(int, port)) + port = list(map(int, port)) - if negative_match: - self.ignore_ports[protocol] = list(set(self.ignore_ports[protocol] + port)) - else: - self.ports[protocol] = list(set(self.ports[protocol] + port)) + if negative_match: + self.ignore_ports[protocol] = list(set(self.ignore_ports[protocol] + port)) + else: + self.ports[protocol] = list(set(self.ports[protocol] + port)) - @final - def add_service_match(self, regex, negative_match=False): - if not isinstance(regex, list): - regex = [regex] + @final + def match_service_name(self, name, negative_match=False): + if not isinstance(name, list): + name = [name] - valid_regex = True - for r in regex: - try: - re.compile(r) - except re.error: - print('Invalid regex: ' + r) - valid_regex = False + valid_regex = True + for r in name: + try: + re.compile(r) + except re.error: + print('Invalid regex: ' + r) + valid_regex = False - if valid_regex: - if negative_match: - self.ignore_services = list(set(self.ignore_services + regex)) - else: - self.services = list(set(self.services + regex)) - else: - sys.exit(1) + if valid_regex: + if negative_match: + self.ignore_service_names = list(set(self.ignore_service_names + name)) + else: + self.service_names = list(set(self.service_names + name)) + else: + sys.exit(1) - @final - def require_ssl(self, boolean): - self.require_ssl_boolean = boolean + @final + def require_ssl(self, boolean): + self.require_ssl_boolean = boolean - @final - def run_once(self, boolean): - self.run_once_boolean = boolean + @final + def run_once(self, boolean): + self.run_once_boolean = boolean class AutoRecon(object): - def __init__(self): - self.pending_targets = [] - self.scanning_targets = [] - self.plugins = {} - self.__slug_regex = re.compile('^[a-z0-9\-]+$') - self.plugin_types = {'port':[], 'service':[]} - self.port_scan_semaphore = None - self.service_scan_semaphore = None - self.argparse = None - self.argparse_group = None - self.args = None - self.tags = [] - self.excluded_tags = [] - self.patterns = [] - self.configurable_keys = ['max_scans', 'max_port_scans', 'single_target', 'outdir', 'only_scans_dir', 'heartbeat', 'timeout', 'target_timeout', 'accessible', 'verbose'] - self.config = { - 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], - 'global_file': os.path.dirname(os.path.realpath(__file__)) + '/global.toml', - 'max_scans': 50, - 'max_port_scans': None, - 'single_target': False, - 'outdir': 'results', - 'only_scans_dir': False, - 'heartbeat': 60, - 'timeout': None, - 'target_timeout': None, - 'accessible': False, - 'verbose': 0 - } - self.lock = asyncio.Lock() - self.load_slug = None - self.load_module = None + def __init__(self): + self.pending_targets = [] + self.scanning_targets = [] + self.plugins = {} + self.__slug_regex = re.compile('^[a-z0-9\-]+$') + self.plugin_types = {'port':[], 'service':[]} + self.port_scan_semaphore = None + self.service_scan_semaphore = None + self.argparse = None + self.argparse_group = None + self.args = None + self.tags = [] + self.excluded_tags = [] + self.patterns = [] + self.configurable_keys = ['max_scans', 'max_port_scans', 'single_target', 'outdir', 'only_scans_dir', 'heartbeat', 'timeout', 'target_timeout', 'accessible', 'verbose'] + self.config = { + 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], + 'global_file': os.path.dirname(os.path.realpath(__file__)) + '/global.toml', + 'max_scans': 50, + 'max_port_scans': None, + 'single_target': False, + 'outdir': 'results', + 'only_scans_dir': False, + 'heartbeat': 60, + 'timeout': None, + 'target_timeout': None, + 'accessible': False, + 'verbose': 0 + } + self.lock = asyncio.Lock() + self.load_slug = None + self.load_module = None - def add_argument(self, plugin, name, **kwargs): - # TODO: make sure name is simple. - name = '--' + plugin.slug + '.' + slugify(name) + def add_argument(self, plugin, name, **kwargs): + # TODO: make sure name is simple. + name = '--' + plugin.slug + '.' + slugify(name) - if self.argparse_group is None: - self.argparse_group = self.argparse.add_argument_group('plugin arguments', description='These are optional arguments for certain plugins.') - self.argparse_group.add_argument(name, **kwargs) + if self.argparse_group is None: + self.argparse_group = self.argparse.add_argument_group('plugin arguments', description='These are optional arguments for certain plugins.') + self.argparse_group.add_argument(name, **kwargs) - def extract_service(self, line, regex): - if regex is None: - regex = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - match = re.search(regex, line) - if match: - protocol = match.group('protocol').lower() - port = int(match.group('port')) - service = match.group('service') - secure = True if 'ssl' in service or 'tls' in service else False + def extract_service(self, line, regex): + if regex is None: + regex = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + match = re.search(regex, line) + if match: + protocol = match.group('protocol').lower() + port = int(match.group('port')) + service = match.group('service') + secure = True if 'ssl' in service or 'tls' in service else False - if service.startswith('ssl/') or service.startswith('tls/'): - service = service[4:] + if service.startswith('ssl/') or service.startswith('tls/'): + service = service[4:] - from autorecon import Service - return Service(protocol, port, service, secure) - else: - return None + from autorecon import Service + return Service(protocol, port, service, secure) + else: + return None - async def extract_services(self, stream, regex): - if not isinstance(stream, CommandStreamReader): - print('Error: extract_services must be passed an instance of a CommandStreamReader.') - sys.exit(1) + async def extract_services(self, stream, regex): + if not isinstance(stream, CommandStreamReader): + print('Error: extract_services must be passed an instance of a CommandStreamReader.') + sys.exit(1) - services = [] - while True: - line = await stream.readline() - if line is not None: - service = self.extract_service(line, regex) - if service: - services.append(service) - else: - break - return services + services = [] + while True: + line = await stream.readline() + if line is not None: + service = self.extract_service(line, regex) + if service: + services.append(service) + else: + break + return services - def register(self, plugin): - if plugin.disabled: - return + def register(self, plugin): + if plugin.disabled: + return - for _, loaded_plugin in self.plugins.items(): - if plugin.name == loaded_plugin.name: - fail('Error: Duplicate plugin name "' + plugin.name + '" detected.', file=sys.stderr) + for _, loaded_plugin in self.plugins.items(): + if plugin.name == loaded_plugin.name: + fail('Error: Duplicate plugin name "' + plugin.name + '" detected.', file=sys.stderr) - if plugin.slug is None: - plugin.slug = slugify(plugin.name) - elif not self.__slug_regex.match(plugin.slug): - fail('Error: provided slug "' + plugin.slug + '" is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) + if plugin.slug is None: + plugin.slug = slugify(plugin.name) + elif not self.__slug_regex.match(plugin.slug): + fail('Error: provided slug "' + plugin.slug + '" is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) - if plugin.slug in self.config['protected_classes']: - fail('Error: plugin slug "' + plugin.slug + '" is a protected string. Please change.') + if plugin.slug in self.config['protected_classes']: + fail('Error: plugin slug "' + plugin.slug + '" is a protected string. Please change.') - if plugin.slug not in self.plugins: + if plugin.slug not in self.plugins: - for _, loaded_plugin in self.plugins.items(): - if plugin is loaded_plugin: - fail('Error: plugin "' + plugin.name + '" already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) + for _, loaded_plugin in self.plugins.items(): + if plugin is loaded_plugin: + fail('Error: plugin "' + plugin.name + '" already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) - if plugin.description is None: - plugin.description = '' + if plugin.description is None: + plugin.description = '' - configure_function_found = False - run_coroutine_found = False - manual_function_found = False + configure_function_found = False + run_coroutine_found = False + manual_function_found = False - for member_name, member_value in inspect.getmembers(plugin, predicate=inspect.ismethod): - if member_name == 'configure': - configure_function_found = True - elif member_name == 'run' and inspect.iscoroutinefunction(member_value): - run_coroutine_found = True - elif member_name == 'manual': - manual_function_found = True + for member_name, member_value in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'configure': + configure_function_found = True + elif member_name == 'run' and inspect.iscoroutinefunction(member_value): + run_coroutine_found = True + elif member_name == 'manual': + manual_function_found = True - if not run_coroutine_found and not manual_function_found: - fail('Error: the plugin "' + plugin.name + '" needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) + if not run_coroutine_found and not manual_function_found: + fail('Error: the plugin "' + plugin.name + '" needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) - from autorecon import PortScan, ServiceScan - if issubclass(plugin.__class__, PortScan): - self.plugin_types["port"].append(plugin) - elif issubclass(plugin.__class__, ServiceScan): - self.plugin_types["service"].append(plugin) - else: - fail('Plugin "' + plugin.name + '" is neither a PortScan nor a ServiceScan.', file=sys.stderr) + from autorecon import PortScan, ServiceScan + if issubclass(plugin.__class__, PortScan): + self.plugin_types["port"].append(plugin) + elif issubclass(plugin.__class__, ServiceScan): + self.plugin_types["service"].append(plugin) + else: + fail('Plugin "' + plugin.name + '" is neither a PortScan nor a ServiceScan.', file=sys.stderr) - plugin.tags = [tag.lower() for tag in plugin.tags] + plugin.tags = [tag.lower() for tag in plugin.tags] - plugin.autorecon = self - if configure_function_found: - plugin.configure() - self.plugins[plugin.slug] = plugin - else: - fail('Error: plugin slug "' + plugin.slug + '" is already assigned.', file=sys.stderr) + plugin.autorecon = self + if configure_function_found: + plugin.configure() + self.plugins[plugin.slug] = plugin + else: + fail('Error: plugin slug "' + plugin.slug + '" is already assigned.', file=sys.stderr) - async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None): - if patterns: - combined_patterns = self.patterns + patterns - else: - combined_patterns = self.patterns + async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None): + if patterns: + combined_patterns = self.patterns + patterns + else: + combined_patterns = self.patterns - process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - executable='/bin/bash') + process = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + executable='/bin/bash') - cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile) - cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile) + cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile) + cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile) - asyncio.create_task(cout._read()) - asyncio.create_task(cerr._read()) + asyncio.create_task(cout._read()) + asyncio.create_task(cerr._read()) - return process, cout, cerr + return process, cout, cerr # Since this file is run as the main method and also imported by plugins, # we need to make sure that only one instance of the AutoRecon is # created. This cannot be done with Singletons unfortunately, which is # why this hack is here. if 'autorecon' not in sys.modules: # If this file is not yet imported, create the AutoRecon object - autorecon = AutoRecon() + autorecon = AutoRecon() else: # Otherwise, assign it from the __main__ module. - autorecon = sys.modules['__main__'].autorecon + autorecon = sys.modules['__main__'].autorecon def e(*args, frame_index=1, **kvargs): - frame = sys._getframe(frame_index) + frame = sys._getframe(frame_index) - vals = {} + vals = {} - vals.update(frame.f_globals) - vals.update(frame.f_locals) - vals.update(kvargs) + vals.update(frame.f_globals) + vals.update(frame.f_locals) + vals.update(kvargs) - return string.Formatter().vformat(' '.join(args), args, vals) + return string.Formatter().vformat(' '.join(args), args, vals) def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, **kvargs): - frame = sys._getframe(frame_index) + frame = sys._getframe(frame_index) - vals = { - 'bgreen': Fore.GREEN + Style.BRIGHT, - 'bred': Fore.RED + Style.BRIGHT, - 'bblue': Fore.BLUE + Style.BRIGHT, - 'byellow': Fore.YELLOW + Style.BRIGHT, - 'bmagenta': Fore.MAGENTA + Style.BRIGHT, + vals = { + 'bgreen': Fore.GREEN + Style.BRIGHT, + 'bred': Fore.RED + Style.BRIGHT, + 'bblue': Fore.BLUE + Style.BRIGHT, + 'byellow': Fore.YELLOW + Style.BRIGHT, + 'bmagenta': Fore.MAGENTA + Style.BRIGHT, - 'green': Fore.GREEN, - 'red': Fore.RED, - 'blue': Fore.BLUE, - 'yellow': Fore.YELLOW, - 'magenta': Fore.MAGENTA, + 'green': Fore.GREEN, + 'red': Fore.RED, + 'blue': Fore.BLUE, + 'yellow': Fore.YELLOW, + 'magenta': Fore.MAGENTA, - 'bright': Style.BRIGHT, - 'srst': Style.NORMAL, - 'crst': Fore.RESET, - 'rst': Style.NORMAL + Fore.RESET - } + 'bright': Style.BRIGHT, + 'srst': Style.NORMAL, + 'crst': Fore.RESET, + 'rst': Style.NORMAL + Fore.RESET + } - if autorecon.config['accessible']: - vals = {'bgreen':'', 'bred':'', 'bblue':'', 'byellow':'', 'bmagenta':'', 'green':'', 'red':'', 'blue':'', 'yellow':'', 'magenta':'', 'bright':'', 'srst':'', 'crst':'', 'rst':''} + if autorecon.config['accessible']: + vals = {'bgreen':'', 'bred':'', 'bblue':'', 'byellow':'', 'bmagenta':'', 'green':'', 'red':'', 'blue':'', 'yellow':'', 'magenta':'', 'bright':'', 'srst':'', 'crst':'', 'rst':''} - vals.update(frame.f_globals) - vals.update(frame.f_locals) - vals.update(kvargs) + vals.update(frame.f_globals) + vals.update(frame.f_locals) + vals.update(kvargs) - unfmt = '' - if char is not None and not autorecon.config['accessible']: - unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep - unfmt += sep.join(args) + unfmt = '' + if char is not None and not autorecon.config['accessible']: + unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep + unfmt += sep.join(args) - fmted = unfmt + fmted = unfmt - for attempt in range(10): - try: - fmted = string.Formatter().vformat(unfmt, args, vals) - break - except KeyError as err: - key = err.args[0] - unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') + for attempt in range(10): + try: + fmted = string.Formatter().vformat(unfmt, args, vals) + break + except KeyError as err: + key = err.args[0] + unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') - if printmsg: - print(fmted, sep=sep, end=end, file=file) - else: - return fmted + if printmsg: + print(fmted, sep=sep, end=end, file=file) + else: + return fmted def debug(*args, color=Fore.GREEN, sep=' ', end='\n', file=sys.stdout, **kvargs): - if verbose >= 2: - if autorecon.config['accessible']: - args = ('Debug:',) + args - cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs) + if verbose >= 2: + if autorecon.config['accessible']: + args = ('Debug:',) + args + cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs) def info(*args, sep=' ', end='\n', file=sys.stdout, **kvargs): - cprint(*args, color=Fore.BLUE, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs) + cprint(*args, color=Fore.BLUE, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs) def warn(*args, sep=' ', end='\n', file=sys.stderr,**kvargs): - if autorecon.config['accessible']: - args = ('Warning:',) + args - cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + if autorecon.config['accessible']: + args = ('Warning:',) + args + cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) def error(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): - if autorecon.config['accessible']: - args = ('Error:',) + args - cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + if autorecon.config['accessible']: + args = ('Error:',) + args + cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): - if autorecon.config['accessible']: - args = ('Failure:',) + args - cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) - exit(-1) + if autorecon.config['accessible']: + args = ('Failure:',) + args + cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + exit(-1) def calculate_elapsed_time(start_time): - elapsed_seconds = round(time.time() - start_time) + elapsed_seconds = round(time.time() - start_time) - m, s = divmod(elapsed_seconds, 60) - h, m = divmod(m, 60) + m, s = divmod(elapsed_seconds, 60) + h, m = divmod(m, 60) - elapsed_time = [] - if h == 1: - elapsed_time.append(str(h) + ' hour') - elif h > 1: - elapsed_time.append(str(h) + ' hours') + elapsed_time = [] + if h == 1: + elapsed_time.append(str(h) + ' hour') + elif h > 1: + elapsed_time.append(str(h) + ' hours') - if m == 1: - elapsed_time.append(str(m) + ' minute') - elif m > 1: - elapsed_time.append(str(m) + ' minutes') + if m == 1: + elapsed_time.append(str(m) + ' minute') + elif m > 1: + elapsed_time.append(str(m) + ' minutes') - if s == 1: - elapsed_time.append(str(s) + ' second') - elif s > 1: - elapsed_time.append(str(s) + ' seconds') - else: - elapsed_time.append('less than a second') + if s == 1: + elapsed_time.append(str(s) + ' second') + elif s > 1: + elapsed_time.append(str(s) + ' seconds') + else: + elapsed_time.append('less than a second') - return ', '.join(elapsed_time) + return ', '.join(elapsed_time) def slugify(name): - return re.sub(r'[\W_]+', '-', unidecode.unidecode(name).lower()).strip('-') + return re.sub(r'[\W_]+', '-', unidecode.unidecode(name).lower()).strip('-') def cancel_all_tasks(signal, frame): - for task in asyncio.all_tasks(): - task.cancel() + for task in asyncio.all_tasks(): + task.cancel() - for target in autorecon.scanning_targets: - for process_list in target.running_tasks.values(): - for process_dict in process_list['processes']: - try: - process_dict['process'].kill() - except ProcessLookupError: # Will get raised if the process finishes before we get to killing it. - pass + for target in autorecon.scanning_targets: + for process_list in target.running_tasks.values(): + for process_dict in process_list['processes']: + try: + process_dict['process'].kill() + except ProcessLookupError: # Will get raised if the process finishes before we get to killing it. + pass async def start_heartbeat(target, period=60): - while True: - await asyncio.sleep(period) - async with target.lock: - count = len(target.running_tasks) + while True: + await asyncio.sleep(period) + async with target.lock: + count = len(target.running_tasks) - tasks_list = '' - if target.autorecon.config['verbose'] >= 1: - tasks_list = ': {bblue}' + ', '.join(target.running_tasks.keys()) + '{rst}' + tasks_list = '' + if target.autorecon.config['verbose'] >= 1: + tasks_list = ': {bblue}' + ', '.join(target.running_tasks.keys()) + '{rst}' - current_time = datetime.now().strftime('%H:%M:%S') + current_time = datetime.now().strftime('%H:%M:%S') - if count > 1: - info('{bgreen}' + current_time + '{rst} - There are {byellow}' + str(count) + '{rst} scans still running against {byellow}' + target.address + '{rst}' + tasks_list) - elif count == 1: - info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) + if count > 1: + info('{bgreen}' + current_time + '{rst} - There are {byellow}' + str(count) + '{rst} scans still running against {byellow}' + target.address + '{rst}' + tasks_list) + elif count == 1: + info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) async def port_scan(plugin, target): - async with target.autorecon.port_scan_semaphore: - info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') + async with target.autorecon.port_scan_semaphore: + info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') - async with target.lock: - target.running_tasks[plugin.slug] = {'plugin': plugin, 'processes':[]} + async with target.lock: + target.running_tasks[plugin.slug] = {'plugin': plugin, 'processes':[]} - start_time = time.time() - try: - result = await plugin.run(target) - except Exception as ex: - exc_type, exc_value, exc_tb = sys.exc_info() - error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) - raise Exception(cprint('Error: Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) + start_time = time.time() + try: + result = await plugin.run(target) + except Exception as ex: + exc_type, exc_value, exc_tb = sys.exc_info() + error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) + raise Exception(cprint('Error: Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) - for process_dict in target.running_tasks[plugin.slug]['processes']: - if process_dict['process'].returncode is None: - warn('A process was left running after port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') - await process_dict['process'].wait() + for process_dict in target.running_tasks[plugin.slug]['processes']: + if process_dict['process'].returncode is None: + warn('A process was left running after port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + await process_dict['process'].wait() - if process_dict['process'].returncode != 0: - errors = [] - while True: - line = await process_dict['stderr'].readline() - if line is not None: - errors.append(line + '\n') - else: - break - error('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.') - async with target.lock: - with open(os.path.join(target.scandir, '_errors.log'), 'a') as file: - file.writelines('[*] Port scan ' + plugin.name + ' (' + plugin.slug + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') - file.writelines('[-] Command: ' + process_dict['cmd'] + '\n') - if errors: - file.writelines(['[-] Error Output:\n'] + errors + ['\n']) - else: - file.writelines('\n') + if process_dict['process'].returncode != 0: + errors = [] + while True: + line = await process_dict['stderr'].readline() + if line is not None: + errors.append(line + '\n') + else: + break + error('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.') + async with target.lock: + with open(os.path.join(target.scandir, '_errors.log'), 'a') as file: + file.writelines('[*] Port scan ' + plugin.name + ' (' + plugin.slug + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') + file.writelines('[-] Command: ' + process_dict['cmd'] + '\n') + if errors: + file.writelines(['[-] Error Output:\n'] + errors + ['\n']) + else: + file.writelines('\n') - elapsed_time = calculate_elapsed_time(start_time) + elapsed_time = calculate_elapsed_time(start_time) - async with target.lock: - target.running_tasks.pop(plugin.slug, None) + async with target.lock: + target.running_tasks.pop(plugin.slug, None) - info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time) - return {'type':'port', 'plugin':plugin, 'result':result} + info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time) + return {'type':'port', 'plugin':plugin, 'result':result} async def service_scan(plugin, service): - from autorecon import PortScan - semaphore = service.target.autorecon.service_scan_semaphore + from autorecon import PortScan + semaphore = service.target.autorecon.service_scan_semaphore - # If service scan semaphore is locked, see if we can use port scan semaphore. - while True: - if semaphore.locked(): - if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans + # If service scan semaphore is locked, see if we can use port scan semaphore. + while True: + if semaphore.locked(): + if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans - port_scan_task_count = 0 - for targ in service.target.autorecon.scanning_targets: - for process_list in targ.running_tasks.values(): - if issubclass(process_list['plugin'].__class__, PortScan): - port_scan_task_count += 1 + port_scan_task_count = 0 + for targ in service.target.autorecon.scanning_targets: + for process_list in targ.running_tasks.values(): + if issubclass(process_list['plugin'].__class__, PortScan): + port_scan_task_count += 1 - if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. - if service.target.autorecon.port_scan_semaphore.locked(): - await asyncio.sleep(1) - continue - semaphore = service.target.autorecon.port_scan_semaphore - break - else: # Do some math to see if we can use the port scan semaphore. - if (service.target.autorecon.config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * service.target.autorecon.config['port_scan_plugin_count']))) >= 1: - if service.target.autorecon.port_scan_semaphore.locked(): - await asyncio.sleep(1) - continue - semaphore = service.target.autorecon.port_scan_semaphore - break - else: - await asyncio.sleep(1) - else: - break - else: - break + if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. + if service.target.autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = service.target.autorecon.port_scan_semaphore + break + else: # Do some math to see if we can use the port scan semaphore. + if (service.target.autorecon.config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * service.target.autorecon.config['port_scan_plugin_count']))) >= 1: + if service.target.autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = service.target.autorecon.port_scan_semaphore + break + else: + await asyncio.sleep(1) + else: + break + else: + break - async with semaphore: - tag = service.tag() + '/' + plugin.slug - info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') + async with semaphore: + tag = service.tag() + '/' + plugin.slug + info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') - async with service.target.lock: - service.target.running_tasks[tag] = {'plugin': plugin, 'processes':[]} + async with service.target.lock: + service.target.running_tasks[tag] = {'plugin': plugin, 'processes':[]} - start_time = time.time() - try: - result = await plugin.run(service) - except Exception as ex: - exc_type, exc_value, exc_tb = sys.exc_info() - error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) - raise Exception(cprint('Error: Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) + start_time = time.time() + try: + result = await plugin.run(service) + except Exception as ex: + exc_type, exc_value, exc_tb = sys.exc_info() + error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) + raise Exception(cprint('Error: Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) - for process_dict in service.target.running_tasks[tag]['processes']: - if process_dict['process'].returncode is None: - warn('A process was left running after service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') - await process_dict['process'].wait() + for process_dict in service.target.running_tasks[tag]['processes']: + if process_dict['process'].returncode is None: + warn('A process was left running after service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + await process_dict['process'].wait() - if process_dict['process'].returncode != 0: - errors = [] - while True: - line = await process_dict['stderr'].readline() - if line is not None: - errors.append(line + '\n') - else: - break - error('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.') - async with service.target.lock: - with open(os.path.join(service.target.scandir, '_errors.log'), 'a') as file: - file.writelines('[*] Service scan ' + plugin.name + ' (' + tag + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') - file.writelines('[-] Command: ' + process_dict['cmd'] + '\n') - if errors: - file.writelines(['[-] Error Output:\n'] + errors + ['\n']) - else: - file.writelines('\n') + if process_dict['process'].returncode != 0: + errors = [] + while True: + line = await process_dict['stderr'].readline() + if line is not None: + errors.append(line + '\n') + else: + break + error('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.') + async with service.target.lock: + with open(os.path.join(service.target.scandir, '_errors.log'), 'a') as file: + file.writelines('[*] Service scan ' + plugin.name + ' (' + tag + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') + file.writelines('[-] Command: ' + process_dict['cmd'] + '\n') + if errors: + file.writelines(['[-] Error Output:\n'] + errors + ['\n']) + else: + file.writelines('\n') - elapsed_time = calculate_elapsed_time(start_time) + elapsed_time = calculate_elapsed_time(start_time) - async with service.target.lock: - service.target.running_tasks.pop(tag, None) + async with service.target.lock: + service.target.running_tasks.pop(tag, None) - info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time) - return {'type':'service', 'plugin':plugin, 'result':result} + info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time) + return {'type':'service', 'plugin':plugin, 'result':result} async def scan_target(target): - if target.autorecon.config['single_target']: - basedir = os.path.abspath(target.autorecon.config['outdir']) - else: - basedir = os.path.abspath(os.path.join(target.autorecon.config['outdir'], target.address)) - target.basedir = basedir - os.makedirs(basedir, exist_ok=True) + if target.autorecon.config['single_target']: + basedir = os.path.abspath(target.autorecon.config['outdir']) + else: + basedir = os.path.abspath(os.path.join(target.autorecon.config['outdir'], target.address)) + target.basedir = basedir + os.makedirs(basedir, exist_ok=True) - if not target.autorecon.config['only_scans_dir']: - exploitdir = os.path.abspath(os.path.join(basedir, 'exploit')) - os.makedirs(exploitdir, exist_ok=True) + if not target.autorecon.config['only_scans_dir']: + exploitdir = os.path.abspath(os.path.join(basedir, 'exploit')) + os.makedirs(exploitdir, exist_ok=True) - lootdir = os.path.abspath(os.path.join(basedir, 'loot')) - os.makedirs(lootdir, exist_ok=True) + lootdir = os.path.abspath(os.path.join(basedir, 'loot')) + os.makedirs(lootdir, exist_ok=True) - reportdir = os.path.abspath(os.path.join(basedir, 'report')) - target.reportdir = reportdir - os.makedirs(reportdir, exist_ok=True) + reportdir = os.path.abspath(os.path.join(basedir, 'report')) + target.reportdir = reportdir + os.makedirs(reportdir, exist_ok=True) - open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close() - open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close() + open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close() + open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close() - screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots')) - os.makedirs(screenshotdir, exist_ok=True) + screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots')) + os.makedirs(screenshotdir, exist_ok=True) - scandir = os.path.abspath(os.path.join(basedir, 'scans')) - target.scandir = scandir - os.makedirs(scandir, exist_ok=True) + scandir = os.path.abspath(os.path.join(basedir, 'scans')) + target.scandir = scandir + os.makedirs(scandir, exist_ok=True) - os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True) + os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True) - pending = [] + pending = [] - heartbeat = asyncio.create_task(start_heartbeat(target, period=target.autorecon.config['heartbeat'])) + heartbeat = asyncio.create_task(start_heartbeat(target, period=target.autorecon.config['heartbeat'])) - for plugin in target.autorecon.plugin_types['port']: - plugin_tag_set = set(plugin.tags) + for plugin in target.autorecon.plugin_types['port']: + plugin_tag_set = set(plugin.tags) - matching_tags = False - for tag_group in target.autorecon.tags: - if set(tag_group).issubset(plugin_tag_set): - matching_tags = True - break + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break - excluded_tags = False - for tag_group in target.autorecon.excluded_tags: - if set(tag_group).issubset(plugin_tag_set): - excluded_tags = True - break + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break - if matching_tags and not excluded_tags: - pending.append(asyncio.create_task(port_scan(plugin, target))) + if matching_tags and not excluded_tags: + pending.append(asyncio.create_task(port_scan(plugin, target))) - async with autorecon.lock: - autorecon.scanning_targets.append(target) + async with autorecon.lock: + autorecon.scanning_targets.append(target) - start_time = time.time() - info('Scanning target {byellow}' + target.address + '{rst}') + start_time = time.time() + info('Scanning target {byellow}' + target.address + '{rst}') - timed_out = False - while pending: - done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + timed_out = False + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) - # Check if global timeout has occurred. - if autorecon.config['target_timeout'] is not None: - elapsed_seconds = round(time.time() - start_time) - m, s = divmod(elapsed_seconds, 60) - if m >= autorecon.config['target_timeout']: - timed_out = True - break + # Check if global timeout has occurred. + if autorecon.config['target_timeout'] is not None: + elapsed_seconds = round(time.time() - start_time) + m, s = divmod(elapsed_seconds, 60) + if m >= autorecon.config['target_timeout']: + timed_out = True + break - # Extract Services - services = [] - async with target.lock: - while target.pending_services: - services.append(target.pending_services.pop(0)) + # Extract Services + services = [] + async with target.lock: + while target.pending_services: + services.append(target.pending_services.pop(0)) - for task in done: - try: - if task.exception(): - print(task.exception()) - continue - except asyncio.InvalidStateError: - pass + for task in done: + try: + if task.exception(): + print(task.exception()) + continue + except asyncio.InvalidStateError: + pass - if task.result()['type'] == 'port': - for service in (task.result()['result'] or []): - services.append(service) + if task.result()['type'] == 'port': + for service in (task.result()['result'] or []): + services.append(service) - for service in services: - if service.full_tag() not in target.services: - target.services.append(service.full_tag()) - else: - continue + for service in services: + if service.full_tag() not in target.services: + target.services.append(service.full_tag()) + else: + continue - info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') + info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') - service.target = target + service.target = target - # Create variables for command references. - address = target.address - scandir = target.scandir - protocol = service.protocol - port = service.port + # Create variables for command references. + address = target.address + scandir = target.scandir + protocol = service.protocol + port = service.port - # Special cases for HTTP. - http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http' + # Special cases for HTTP. + http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http' - nmap_extra = target.autorecon.args.nmap - if target.autorecon.args.nmap_append: - nmap_extra += ' ' + target.autorecon.args.nmap_append + nmap_extra = target.autorecon.args.nmap + if target.autorecon.args.nmap_append: + nmap_extra += ' ' + target.autorecon.args.nmap_append - if protocol == 'udp': - nmap_extra += ' -sU' + if protocol == 'udp': + nmap_extra += ' -sU' - matching_plugins = [] - heading = False + matching_plugins = [] + heading = False - for plugin in target.autorecon.plugin_types['service']: - plugin_tag = service.tag() + '/' + plugin.slug + for plugin in target.autorecon.plugin_types['service']: + plugin_tag = service.tag() + '/' + plugin.slug - for s in plugin.services: - if re.search(s, service.name): - plugin_tag_set = set(plugin.tags) + for s in plugin.service_names: + if re.search(s, service.name): + plugin_tag_set = set(plugin.tags) - matching_tags = False - for tag_group in target.autorecon.tags: - if set(tag_group).issubset(plugin_tag_set): - matching_tags = True - break + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break - excluded_tags = False - for tag_group in target.autorecon.excluded_tags: - if set(tag_group).issubset(plugin_tag_set): - excluded_tags = True - break + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break - # TODO: Maybe make this less messy, keep manual-only plugins separate? - plugin_is_runnable = False - for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): - if member_name == 'run': - plugin_is_runnable = True - break + # TODO: Maybe make this less messy, keep manual-only plugins separate? + plugin_is_runnable = False + for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'run': + plugin_is_runnable = True + break - if plugin_is_runnable and matching_tags and not excluded_tags: - # Skip plugin if run_once_boolean and plugin already in target scans - if plugin.run_once_boolean and (plugin.slug,) in target.scans: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') - continue + if plugin_is_runnable and matching_tags and not excluded_tags: + # Skip plugin if run_once_boolean and plugin already in target scans + if plugin.run_once_boolean and (plugin.slug,) in target.scans: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') + continue - # Skip plugin if require_ssl_boolean and port is not secure - if plugin.require_ssl_boolean and not service.secure: - continue + # Skip plugin if require_ssl_boolean and port is not secure + if plugin.require_ssl_boolean and not service.secure: + continue - # Skip plugin if service port is in ignore_ports: - if port in plugin.ignore_ports[protocol]: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') - continue + # Skip plugin if service port is in ignore_ports: + if port in plugin.ignore_ports[protocol]: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') + continue - # Skip plugin if plugin has required ports and service port is not in them: - if plugin.ports[protocol] and port not in plugin.ports[protocol]: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin can only run on specific ports. Skipping.{rst}') - continue + # Skip plugin if plugin has required ports and service port is not in them: + if plugin.ports[protocol] and port not in plugin.ports[protocol]: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin can only run on specific ports. Skipping.{rst}') + continue - for i in plugin.ignore_services: - if re.search(i, service.name): - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against this service. Skipping.{rst}') - continue + for i in plugin.ignore_service_names: + if re.search(i, service.name): + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against this service. Skipping.{rst}') + continue - # TODO: check if plugin matches tags, BUT run manual commands anyway! - matching_plugins.append(plugin) + # TODO: check if plugin matches tags, BUT run manual commands anyway! + matching_plugins.append(plugin) - if plugin.manual_commands and (not plugin.run_once_boolean or (plugin.run_once_boolean and (plugin.slug,) not in target.scans)): - with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: - if not heading: - file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) - heading = True - for description, commands in plugin.manual_commands.items(): - file.write('\t[-] ' + e(description) + '\n\n') - for command in commands: - file.write('\t\t' + e(command) + '\n\n') - file.flush() + if plugin.manual_commands and (not plugin.run_once_boolean or (plugin.run_once_boolean and (plugin.slug,) not in target.scans)): + with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: + if not heading: + file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) + heading = True + for description, commands in plugin.manual_commands.items(): + file.write('\t[-] ' + e(description) + '\n\n') + for command in commands: + file.write('\t\t' + e(command) + '\n\n') + file.flush() - break + break - for plugin in matching_plugins: - plugin_tag = service.tag() + '/' + plugin.slug + for plugin in matching_plugins: + plugin_tag = service.tag() + '/' + plugin.slug - scan_tuple = (service.protocol, service.port, service.name, plugin.slug) - if plugin.run_once_boolean: - scan_tuple = (plugin.slug,) + scan_tuple = (service.protocol, service.port, service.name, plugin.slug) + if plugin.run_once_boolean: + scan_tuple = (plugin.slug,) - if scan_tuple in target.scans: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') - continue - else: - target.scans.append(scan_tuple) + if scan_tuple in target.scans: + warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') + continue + else: + target.scans.append(scan_tuple) - pending.add(asyncio.create_task(service_scan(plugin, service))) - heartbeat.cancel() - elapsed_time = calculate_elapsed_time(start_time) + pending.add(asyncio.create_task(service_scan(plugin, service))) + heartbeat.cancel() + elapsed_time = calculate_elapsed_time(start_time) - if timed_out: + if timed_out: - for task in pending: - task.cancel() + for task in pending: + task.cancel() - for process_list in target.running_tasks.values(): - for process_dict in process_list['processes']: - process_dict['process'].kill() + for process_list in target.running_tasks.values(): + for process_dict in process_list['processes']: + process_dict['process'].kill() - warn('{byellow}Scanning target ' + target.address + ' took longer than the specified target period (' + str(autorecon.config['target_timeout']) + ' min). Cancelling scans and moving to next target.{rst}') - else: - info('Finished scanning target {byellow}' + target.address + '{rst} in ' + elapsed_time) + warn('{byellow}Scanning target ' + target.address + ' took longer than the specified target period (' + str(autorecon.config['target_timeout']) + ' min). Cancelling scans and moving to next target.{rst}') + else: + info('Finished scanning target {byellow}' + target.address + '{rst} in ' + elapsed_time) - async with autorecon.lock: - autorecon.scanning_targets.remove(target) + async with autorecon.lock: + autorecon.scanning_targets.remove(target) async def main(): - from autorecon import Plugin, PortScan, ServiceScan, Target # We have to do this to get around issubclass weirdness when loading plugins. - - parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') - parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') - parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') - parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: 50') - parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') - parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') - parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') - parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group.') - parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group.') - parser.add_argument('--plugins-dir', action='store', type=str, default=os.path.dirname(os.path.abspath(__file__)) + '/plugins', help='') - parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: results') - parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: false') - parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: false') - parser.add_argument('--heartbeat', action='store', type=int, help='Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60') - parser.add_argument('--timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: no timeout') - parser.add_argument('--target-timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: no timeout') - nmap_group = parser.add_mutually_exclusive_group() - nmap_group.add_argument('--nmap', action='store', default='-vv --reason -Pn', help='Override the {nmap_extra} variable in scans. Default: %(default)s') - nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.') - parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running. Default: false') - parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders.') - parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') - parser.error = lambda s: fail(s[0].upper() + s[1:]) - args, unknown = parser.parse_known_args() - - errors = False - - autorecon.argparse = parser - - # Parse config file and args for global.toml first. - if not os.path.isfile(args.config_file): - fail('Error: Specified config file "' + args.config_file + '" does not exist.') - - with open(args.config_file) as c: - try: - config_toml = toml.load(c) - for key, val in config_toml.items(): - if key.replace('-', '_') == 'global_file': - autorecon.config['global_file'] = val - break - except toml.decoder.TomlDecodeError: - fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.') - - args_dict = vars(args) - for key in args_dict: - if key == 'global_file' and args_dict['global_file'] is not None: - autorecon.config['global_file'] = args_dict['global_file'] - break - - if not os.path.isdir(args.plugins_dir): - fail('Error: Specified plugins directory "' + args.plugins_dir + '" does not exist.') - - for plugin_file in os.listdir(args.plugins_dir): - if not plugin_file.startswith('_') and plugin_file.endswith('.py'): - - dirname, filename = os.path.split(plugin_file) - dirname = os.path.abspath(dirname) - - try: - plugin = importlib.import_module('.' + filename[:-3], os.path.basename(args.plugins_dir)) - clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) - for (_, c) in clsmembers: - if c.__module__ == 'autorecon': - continue - - if c.__name__.lower() in autorecon.config['protected_classes']: - print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.') - sys.exit(1) - - # Only add classes that are a sub class of either PortScan or ServiceScan - if issubclass(c, PortScan) or issubclass(c, ServiceScan): - autorecon.register(c()) - else: - print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.') - except (ImportError, SyntaxError) as ex: - print('cannot import ' + filename + ' plugin') - print(ex) - sys.exit(1) - - if len(autorecon.plugin_types['port']) == 0: - fail('Error: There are no valid PortScan plugins in the plugins directory "' + args.plugins_dir + '".') - - # Sort plugins by priority. - autorecon.plugin_types['port'].sort(key=lambda x: x.priority) - autorecon.plugin_types['service'].sort(key=lambda x: x.priority) - - if not os.path.isfile(autorecon.config['global_file']): - fail('Error: Specified global file "' + autorecon.config['global_file'] + '" does not exist.') - - global_plugin_args = None - with open(autorecon.config['global_file']) as g: - try: - global_toml = toml.load(g) - for key, val in global_toml.items(): - if key == 'global' and isinstance(val, dict): # Process global plugin options. - for gkey, gvals in global_toml['global'].items(): - if isinstance(gvals, dict): - options = {'metavar':'VALUE'} - - if 'default' in gvals: - options['default'] = gvals['default'] - - if 'metavar' in gvals: - options['metavar'] = gvals['metavar'] - - if 'help' in gvals: - options['help'] = gvals['help'] - - if 'type' in gvals: - gtype = gvals['type'].lower() - if gtype == 'constant': - if 'constant' not in gvals: - fail('Global constant option ' + gkey + ' has no constant value set.') - else: - options['action'] = 'store_const' - options['const'] = gvals['constant'] - elif gtype == 'true': - options['action'] = 'store_true' - options.pop('metavar', None) - options.pop('default', None) - elif gtype == 'false': - options['action'] = 'store_false' - options.pop('metavar', None) - options.pop('default', None) - elif gtype == 'list': - options['action'] = 'append' - elif gtype == 'choice': - if 'choices' not in gvals: - fail('Global choice option ' + gkey + ' has no choices value set.') - else: - if not isinstance(gvals['choices'], list): - fail('The \'choices\' value for global choice option ' + gkey + ' should be a list.') - options['choices'] = gvals['choices'] - options.pop('metavar', None) - - if global_plugin_args is None: - global_plugin_args = parser.add_argument_group("global plugin arguments", description="These are optional arguments that can be used by all plugins.") - - global_plugin_args.add_argument('--global.' + slugify(gkey), **options) - - except toml.decoder.TomlDecodeError: - fail('Error: Couldn\'t parse ' + g.name + ' file. Check syntax.') - - for key, val in config_toml.items(): - if key == 'global' and isinstance(val, dict): # Process global plugin options. - for gkey, gval in config_toml['global'].items(): - if isinstance(gval, bool): - for action in autorecon.argparse._actions: - if action.dest == 'global.' + slugify(gkey).replace('-', '_'): - if action.const is True: - action.__setattr__('default', gval) - break - else: - if autorecon.argparse.get_default('global.' + slugify(gkey).replace('-', '_')): - autorecon.argparse.set_defaults(**{'global.' + slugify(gkey).replace('-', '_'): gval}) - - elif key == 'pattern' and isinstance(val, list): # Process global patterns. - for pattern in val: - if 'pattern' in pattern: - try: - compiled = re.compile(pattern['pattern']) - if 'description' in pattern: - autorecon.patterns.append(Pattern(compiled, description=pattern['description'])) - else: - autorecon.patterns.append(Pattern(compiled)) - except re.error: - fail('Error: The pattern "' + pattern['pattern'] + '" in the config file is invalid regex.') - else: - fail('Error: A [[pattern]] in the config file doesn\'t have a required pattern variable.') - elif isinstance(val, dict): # Process potential plugin arguments. - for pkey, pval in config_toml[key].items(): - if autorecon.argparse.get_default(slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_')): - autorecon.argparse.set_defaults(**{slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): pval}) - else: # Process potential other options. - if key.replace('-', '_') in autorecon.configurable_keys: - autorecon.config[key.replace('-', '_')] = val - autorecon.argparse.set_defaults(**{key.replace('-', '_'): val}) - - parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='Show this help message and exit.') - args = parser.parse_args() - - args_dict = vars(args) - for key in args_dict: - if key in autorecon.configurable_keys and args_dict[key] is not None: - # Special case for booleans - if key in ['accessible', 'single_target', 'only_scans_dir'] and autorecon.config[key]: - continue - autorecon.config[key] = args_dict[key] - - autorecon.args = args - - if autorecon.config['max_scans'] <= 0: - error('Argument -m/--max-scans must be at least 1.') - errors = True - - if autorecon.config['max_port_scans'] is None: - autorecon.config['max_port_scans'] = max(1, round(autorecon.config['max_scans'] * 0.2)) - else: - if autorecon.config['max_port_scans'] <= 0: - error('Argument -mp/--max-port-scans must be at least 1.') - errors = True - - if autorecon.config['max_port_scans'] > autorecon.config['max_scans']: - error('Argument -mp/--max-port-scans cannot be greater than argument -m/--max-scans.') - errors = True - - if autorecon.config['heartbeat'] <= 0: - error('Argument --heartbeat must be at least 1.') - errors = True - - if autorecon.config['timeout'] is not None and autorecon.config['timeout'] <= 0: - error('Argument --timeout must be at least 1.') - errors = True - - if autorecon.config['target_timeout'] is not None and autorecon.config['target_timeout'] <= 0: - error('Argument --target-timeout must be at least 1.') - errors = True - - if autorecon.config['timeout'] is not None and autorecon.config['target_timeout'] is not None and autorecon.config['timeout'] < autorecon.config['target_timeout']: - error('Argument --timeout cannot be less than --target-timeout.') - errors = True - - if not errors: - autorecon.port_scan_semaphore = asyncio.Semaphore(autorecon.config['max_port_scans']) - # If max scans and max port scans is the same, the service scan semaphore and port scan semaphore should be the same object - if autorecon.config['max_scans'] == autorecon.config['max_port_scans']: - autorecon.service_scan_semaphore = autorecon.port_scan_semaphore - else: - autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans'] - autorecon.config['max_port_scans']) - - tags = [] - for tag_group in list(set(filter(None, args.tags.lower().split(',')))): - tags.append(list(set(filter(None, tag_group.split('+'))))) - - # Remove duplicate lists from list. - [autorecon.tags.append(t) for t in tags if t not in autorecon.tags] - - excluded_tags = [] - if args.exclude_tags != '': - for tag_group in list(set(filter(None, args.exclude_tags.lower().split(',')))): - excluded_tags.append(list(set(filter(None, tag_group.split('+'))))) - - # Remove duplicate lists from list. - [autorecon.excluded_tags.append(t) for t in excluded_tags if t not in autorecon.excluded_tags] - - # Generate manual commands. - for _, plugin in autorecon.plugins.items(): - for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): - if member_name == 'manual': - plugin.manual() - - raw_targets = args.targets - - if len(args.target_file) > 0: - if not os.path.isfile(args.target_file): - error('The target file ' + args.target_file + ' was not found.') - sys.exit(1) - try: - with open(args.target_file, 'r') as f: - lines = f.read() - for line in lines.splitlines(): - line = line.strip() - if line.startswith('#') or len(line) == 0: continue - if line not in raw_targets: - raw_targets.append(line) - except OSError: - error('The target file ' + args.target_file + ' could not be read.') - sys.exit(1) - - for target in raw_targets: - try: - ip = str(ipaddress.ip_address(target)) - - if ip not in autorecon.pending_targets: - autorecon.pending_targets.append(ip) - except ValueError: - - try: - target_range = ipaddress.ip_network(target, strict=False) - if not args.disable_sanity_checks and target_range.num_addresses > 256: - error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') - errors = True - else: - for ip in target_range.hosts(): - ip = str(ip) - if ip not in autorecon.pending_targets: - autorecon.pending_targets.append(ip) - except ValueError: - - try: - ip = socket.gethostbyname(target) - - if target not in autorecon.pending_targets: - autorecon.pending_targets.append(target) - except socket.gaierror: - error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') - errors = True - - if len(autorecon.pending_targets) == 0: - error('You must specify at least one target to scan!') - errors = True - - if autorecon.config['single_target'] and len(autorecon.pending_targets) != 1: - error('You cannot provide more than one target when scanning in single-target mode.') - errors = True - - if not args.disable_sanity_checks and len(autorecon.pending_targets) > 256: - error('A total of ' + str(len(autorecon.pending_targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.') - errors = True - - port_scan_plugin_count = 0 - for plugin in autorecon.plugin_types['port']: - matching_tags = False - for tag_group in autorecon.tags: - if set(tag_group).issubset(set(plugin.tags)): - matching_tags = True - break - - excluded_tags = False - for tag_group in autorecon.excluded_tags: - if set(tag_group).issubset(set(plugin.tags)): - excluded_tags = True - break - - if matching_tags and not excluded_tags: - port_scan_plugin_count += 1 - - if port_scan_plugin_count == 0: - error('There are no port scan plugins that match the tags specified.') - errors = True - - if errors: - sys.exit(1) - - autorecon.config['port_scan_plugin_count'] = port_scan_plugin_count - - num_initial_targets = max(1, math.ceil(autorecon.config['max_port_scans'] / port_scan_plugin_count)) - - start_time = time.time() - - pending = [] - i = 0 - while autorecon.pending_targets: - pending.append(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) - i+=1 - if i >= num_initial_targets: - break - - timed_out = False - while pending: - done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) - # Check if global timeout has occurred. - if autorecon.config['timeout'] is not None: - elapsed_seconds = round(time.time() - start_time) - m, s = divmod(elapsed_seconds, 60) - if m >= autorecon.config['timeout']: - timed_out = True - break - - for task in done: - if autorecon.pending_targets: - pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) - - port_scan_task_count = 0 - for targ in autorecon.scanning_targets: - for process_list in targ.running_tasks.values(): - if issubclass(process_list['plugin'].__class__, PortScan): - port_scan_task_count += 1 - - num_new_targets = math.ceil((autorecon.config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count) - if num_new_targets > 0: - i = 0 - while autorecon.pending_targets: - pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) - i+=1 - if i >= num_new_targets: - break - - if timed_out: - cancel_all_tasks(None, None) - - elapsed_time = calculate_elapsed_time(start_time) - warn('{byellow}AutoRecon took longer than the specified timeout period (' + str(autorecon.config['timeout']) + ' min). Cancelling all scans and exiting.{rst}') - sys.exit(0) - else: - while len(asyncio.all_tasks()) > 1: # this code runs in the main() task so it will be the only task left running - await asyncio.sleep(1) - - elapsed_time = calculate_elapsed_time(start_time) - info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}') + from autorecon import Plugin, PortScan, ServiceScan, Target # We have to do this to get around issubclass weirdness when loading plugins. + + parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') + parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') + parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') + parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: 50') + parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') + parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') + parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') + parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group.') + parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group.') + parser.add_argument('--plugins-dir', action='store', type=str, default=os.path.dirname(os.path.abspath(__file__)) + '/plugins', help='') + parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: results') + parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: false') + parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: false') + parser.add_argument('--heartbeat', action='store', type=int, help='Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60') + parser.add_argument('--timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: no timeout') + parser.add_argument('--target-timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: no timeout') + nmap_group = parser.add_mutually_exclusive_group() + nmap_group.add_argument('--nmap', action='store', default='-vv --reason -Pn', help='Override the {nmap_extra} variable in scans. Default: %(default)s') + nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.') + parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running. Default: false') + parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders.') + parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') + parser.error = lambda s: fail(s[0].upper() + s[1:]) + args, unknown = parser.parse_known_args() + + errors = False + + autorecon.argparse = parser + + # Parse config file and args for global.toml first. + if not os.path.isfile(args.config_file): + fail('Error: Specified config file "' + args.config_file + '" does not exist.') + + with open(args.config_file) as c: + try: + config_toml = toml.load(c) + for key, val in config_toml.items(): + if key.replace('-', '_') == 'global_file': + autorecon.config['global_file'] = val + break + except toml.decoder.TomlDecodeError: + fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.') + + args_dict = vars(args) + for key in args_dict: + if key == 'global_file' and args_dict['global_file'] is not None: + autorecon.config['global_file'] = args_dict['global_file'] + break + + if not os.path.isdir(args.plugins_dir): + fail('Error: Specified plugins directory "' + args.plugins_dir + '" does not exist.') + + for plugin_file in os.listdir(args.plugins_dir): + if not plugin_file.startswith('_') and plugin_file.endswith('.py'): + + dirname, filename = os.path.split(plugin_file) + dirname = os.path.abspath(dirname) + + try: + plugin = importlib.import_module('.' + filename[:-3], os.path.basename(args.plugins_dir)) + clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) + for (_, c) in clsmembers: + if c.__module__ == 'autorecon': + continue + + if c.__name__.lower() in autorecon.config['protected_classes']: + print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.') + sys.exit(1) + + # Only add classes that are a sub class of either PortScan or ServiceScan + if issubclass(c, PortScan) or issubclass(c, ServiceScan): + autorecon.register(c()) + else: + print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.') + except (ImportError, SyntaxError) as ex: + print('cannot import ' + filename + ' plugin') + print(ex) + sys.exit(1) + + if len(autorecon.plugin_types['port']) == 0: + fail('Error: There are no valid PortScan plugins in the plugins directory "' + args.plugins_dir + '".') + + # Sort plugins by priority. + autorecon.plugin_types['port'].sort(key=lambda x: x.priority) + autorecon.plugin_types['service'].sort(key=lambda x: x.priority) + + if not os.path.isfile(autorecon.config['global_file']): + fail('Error: Specified global file "' + autorecon.config['global_file'] + '" does not exist.') + + global_plugin_args = None + with open(autorecon.config['global_file']) as g: + try: + global_toml = toml.load(g) + for key, val in global_toml.items(): + if key == 'global' and isinstance(val, dict): # Process global plugin options. + for gkey, gvals in global_toml['global'].items(): + if isinstance(gvals, dict): + options = {'metavar':'VALUE'} + + if 'default' in gvals: + options['default'] = gvals['default'] + + if 'metavar' in gvals: + options['metavar'] = gvals['metavar'] + + if 'help' in gvals: + options['help'] = gvals['help'] + + if 'type' in gvals: + gtype = gvals['type'].lower() + if gtype == 'constant': + if 'constant' not in gvals: + fail('Global constant option ' + gkey + ' has no constant value set.') + else: + options['action'] = 'store_const' + options['const'] = gvals['constant'] + elif gtype == 'true': + options['action'] = 'store_true' + options.pop('metavar', None) + options.pop('default', None) + elif gtype == 'false': + options['action'] = 'store_false' + options.pop('metavar', None) + options.pop('default', None) + elif gtype == 'list': + options['action'] = 'append' + elif gtype == 'choice': + if 'choices' not in gvals: + fail('Global choice option ' + gkey + ' has no choices value set.') + else: + if not isinstance(gvals['choices'], list): + fail('The \'choices\' value for global choice option ' + gkey + ' should be a list.') + options['choices'] = gvals['choices'] + options.pop('metavar', None) + + if global_plugin_args is None: + global_plugin_args = parser.add_argument_group("global plugin arguments", description="These are optional arguments that can be used by all plugins.") + + global_plugin_args.add_argument('--global.' + slugify(gkey), **options) + + except toml.decoder.TomlDecodeError: + fail('Error: Couldn\'t parse ' + g.name + ' file. Check syntax.') + + for key, val in config_toml.items(): + if key == 'global' and isinstance(val, dict): # Process global plugin options. + for gkey, gval in config_toml['global'].items(): + if isinstance(gval, bool): + for action in autorecon.argparse._actions: + if action.dest == 'global.' + slugify(gkey).replace('-', '_'): + if action.const is True: + action.__setattr__('default', gval) + break + else: + if autorecon.argparse.get_default('global.' + slugify(gkey).replace('-', '_')): + autorecon.argparse.set_defaults(**{'global.' + slugify(gkey).replace('-', '_'): gval}) + + elif key == 'pattern' and isinstance(val, list): # Process global patterns. + for pattern in val: + if 'pattern' in pattern: + try: + compiled = re.compile(pattern['pattern']) + if 'description' in pattern: + autorecon.patterns.append(Pattern(compiled, description=pattern['description'])) + else: + autorecon.patterns.append(Pattern(compiled)) + except re.error: + fail('Error: The pattern "' + pattern['pattern'] + '" in the config file is invalid regex.') + else: + fail('Error: A [[pattern]] in the config file doesn\'t have a required pattern variable.') + elif isinstance(val, dict): # Process potential plugin arguments. + for pkey, pval in config_toml[key].items(): + if autorecon.argparse.get_default(slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_')): + autorecon.argparse.set_defaults(**{slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): pval}) + else: # Process potential other options. + if key.replace('-', '_') in autorecon.configurable_keys: + autorecon.config[key.replace('-', '_')] = val + autorecon.argparse.set_defaults(**{key.replace('-', '_'): val}) + + parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='Show this help message and exit.') + args = parser.parse_args() + + args_dict = vars(args) + for key in args_dict: + if key in autorecon.configurable_keys and args_dict[key] is not None: + # Special case for booleans + if key in ['accessible', 'single_target', 'only_scans_dir'] and autorecon.config[key]: + continue + autorecon.config[key] = args_dict[key] + + autorecon.args = args + + if autorecon.config['max_scans'] <= 0: + error('Argument -m/--max-scans must be at least 1.') + errors = True + + if autorecon.config['max_port_scans'] is None: + autorecon.config['max_port_scans'] = max(1, round(autorecon.config['max_scans'] * 0.2)) + else: + if autorecon.config['max_port_scans'] <= 0: + error('Argument -mp/--max-port-scans must be at least 1.') + errors = True + + if autorecon.config['max_port_scans'] > autorecon.config['max_scans']: + error('Argument -mp/--max-port-scans cannot be greater than argument -m/--max-scans.') + errors = True + + if autorecon.config['heartbeat'] <= 0: + error('Argument --heartbeat must be at least 1.') + errors = True + + if autorecon.config['timeout'] is not None and autorecon.config['timeout'] <= 0: + error('Argument --timeout must be at least 1.') + errors = True + + if autorecon.config['target_timeout'] is not None and autorecon.config['target_timeout'] <= 0: + error('Argument --target-timeout must be at least 1.') + errors = True + + if autorecon.config['timeout'] is not None and autorecon.config['target_timeout'] is not None and autorecon.config['timeout'] < autorecon.config['target_timeout']: + error('Argument --timeout cannot be less than --target-timeout.') + errors = True + + if not errors: + autorecon.port_scan_semaphore = asyncio.Semaphore(autorecon.config['max_port_scans']) + # If max scans and max port scans is the same, the service scan semaphore and port scan semaphore should be the same object + if autorecon.config['max_scans'] == autorecon.config['max_port_scans']: + autorecon.service_scan_semaphore = autorecon.port_scan_semaphore + else: + autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans'] - autorecon.config['max_port_scans']) + + tags = [] + for tag_group in list(set(filter(None, args.tags.lower().split(',')))): + tags.append(list(set(filter(None, tag_group.split('+'))))) + + # Remove duplicate lists from list. + [autorecon.tags.append(t) for t in tags if t not in autorecon.tags] + + excluded_tags = [] + if args.exclude_tags != '': + for tag_group in list(set(filter(None, args.exclude_tags.lower().split(',')))): + excluded_tags.append(list(set(filter(None, tag_group.split('+'))))) + + # Remove duplicate lists from list. + [autorecon.excluded_tags.append(t) for t in excluded_tags if t not in autorecon.excluded_tags] + + # Generate manual commands. + for _, plugin in autorecon.plugins.items(): + for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'manual': + plugin.manual() + + raw_targets = args.targets + + if len(args.target_file) > 0: + if not os.path.isfile(args.target_file): + error('The target file ' + args.target_file + ' was not found.') + sys.exit(1) + try: + with open(args.target_file, 'r') as f: + lines = f.read() + for line in lines.splitlines(): + line = line.strip() + if line.startswith('#') or len(line) == 0: continue + if line not in raw_targets: + raw_targets.append(line) + except OSError: + error('The target file ' + args.target_file + ' could not be read.') + sys.exit(1) + + for target in raw_targets: + try: + ip = str(ipaddress.ip_address(target)) + + if ip not in autorecon.pending_targets: + autorecon.pending_targets.append(ip) + except ValueError: + + try: + target_range = ipaddress.ip_network(target, strict=False) + if not args.disable_sanity_checks and target_range.num_addresses > 256: + error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') + errors = True + else: + for ip in target_range.hosts(): + ip = str(ip) + if ip not in autorecon.pending_targets: + autorecon.pending_targets.append(ip) + except ValueError: + + try: + ip = socket.gethostbyname(target) + + if target not in autorecon.pending_targets: + autorecon.pending_targets.append(target) + except socket.gaierror: + error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') + errors = True + + if len(autorecon.pending_targets) == 0: + error('You must specify at least one target to scan!') + errors = True + + if autorecon.config['single_target'] and len(autorecon.pending_targets) != 1: + error('You cannot provide more than one target when scanning in single-target mode.') + errors = True + + if not args.disable_sanity_checks and len(autorecon.pending_targets) > 256: + error('A total of ' + str(len(autorecon.pending_targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.') + errors = True + + port_scan_plugin_count = 0 + for plugin in autorecon.plugin_types['port']: + matching_tags = False + for tag_group in autorecon.tags: + if set(tag_group).issubset(set(plugin.tags)): + matching_tags = True + break + + excluded_tags = False + for tag_group in autorecon.excluded_tags: + if set(tag_group).issubset(set(plugin.tags)): + excluded_tags = True + break + + if matching_tags and not excluded_tags: + port_scan_plugin_count += 1 + + if port_scan_plugin_count == 0: + error('There are no port scan plugins that match the tags specified.') + errors = True + + if errors: + sys.exit(1) + + autorecon.config['port_scan_plugin_count'] = port_scan_plugin_count + + num_initial_targets = max(1, math.ceil(autorecon.config['max_port_scans'] / port_scan_plugin_count)) + + start_time = time.time() + + pending = [] + i = 0 + while autorecon.pending_targets: + pending.append(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + i+=1 + if i >= num_initial_targets: + break + + timed_out = False + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + # Check if global timeout has occurred. + if autorecon.config['timeout'] is not None: + elapsed_seconds = round(time.time() - start_time) + m, s = divmod(elapsed_seconds, 60) + if m >= autorecon.config['timeout']: + timed_out = True + break + + for task in done: + if autorecon.pending_targets: + pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + + port_scan_task_count = 0 + for targ in autorecon.scanning_targets: + for process_list in targ.running_tasks.values(): + if issubclass(process_list['plugin'].__class__, PortScan): + port_scan_task_count += 1 + + num_new_targets = math.ceil((autorecon.config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count) + if num_new_targets > 0: + i = 0 + while autorecon.pending_targets: + pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + i+=1 + if i >= num_new_targets: + break + + if timed_out: + cancel_all_tasks(None, None) + + elapsed_time = calculate_elapsed_time(start_time) + warn('{byellow}AutoRecon took longer than the specified timeout period (' + str(autorecon.config['timeout']) + ' min). Cancelling all scans and exiting.{rst}') + sys.exit(0) + else: + while len(asyncio.all_tasks()) > 1: # this code runs in the main() task so it will be the only task left running + await asyncio.sleep(1) + + elapsed_time = calculate_elapsed_time(start_time) + info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}') if __name__ == '__main__': - signal.signal(signal.SIGINT, cancel_all_tasks) - try: - asyncio.run(main()) - except asyncio.exceptions.CancelledError: - pass + signal.signal(signal.SIGINT, cancel_all_tasks) + try: + asyncio.run(main()) + except asyncio.exceptions.CancelledError: + pass diff --git a/plugins/databases.py b/plugins/databases.py index 7930bb2..e96d8a6 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -2,121 +2,121 @@ from autorecon import ServiceScan class NmapMongoDB(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap MongoDB" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Nmap MongoDB" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match('^mongod') + def configure(self): + self.match_service_name('^mongod') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mongodb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mongodb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mongodb_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mongodb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mongodb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mongodb_nmap.xml" {address}') class NmapMSSQL(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap MSSQL" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Nmap MSSQL" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match(['^mssql', '^ms\-sql']) + def configure(self): + self.match_service_name(['^mssql', '^ms\-sql']) - def manual(self): - self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + def manual(self): + self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}') class NmapMYSQL(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap MYSQL" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Nmap MYSQL" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match('^mysql') + def configure(self): + self.match_service_name('^mysql') - def manual(self): - self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + def manual(self): + self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}') class NmapOracle(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap Oracle" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Nmap Oracle" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match('^oracle') + def configure(self): + self.match_service_name('^oracle') - def manual(self): - self.add_manual_command('Brute-force SIDs using Nmap:', 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}') + def manual(self): + self.add_manual_command('Brute-force SIDs using Nmap:', 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}') class OracleTNScmd(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Oracle TNScmd" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Oracle TNScmd" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match('^oracle') + def configure(self): + self.match_service_name('^oracle') - async def run(self, service): - await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') - await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') + async def run(self, service): + await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') + await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') class OracleScanner(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Oracle Scanner" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Oracle Scanner" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match('^oracle') + def configure(self): + self.match_service_name('^oracle') - async def run(self, service): - await service.execute('oscanner -v -s {address} -P {port} 2>&1', outfile='{protocol}_{port}_oracle_scanner.txt') + async def run(self, service): + await service.execute('oscanner -v -s {address} -P {port} 2>&1', outfile='{protocol}_{port}_oracle_scanner.txt') class OracleODAT(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Oracle ODAT" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Oracle ODAT" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match('^oracle') + def configure(self): + self.match_service_name('^oracle') - def manual(self): - self.add_manual_commands('Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:', [ - 'python odat.py tnscmd -s {address} -p {port} --ping', - 'python odat.py tnscmd -s {address} -p {port} --version', - 'python odat.py tnscmd -s {address} -p {port} --status', - 'python odat.py sidguesser -s {address} -p {port}', - 'python odat.py passwordguesser -s {address} -p {port} -d --accounts-file accounts/accounts_multiple.txt', - 'python odat.py tnspoison -s {address} -p {port} -d --test-module' - ]) + def manual(self): + self.add_manual_commands('Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:', [ + 'python odat.py tnscmd -s {address} -p {port} --ping', + 'python odat.py tnscmd -s {address} -p {port} --version', + 'python odat.py tnscmd -s {address} -p {port} --status', + 'python odat.py sidguesser -s {address} -p {port}', + 'python odat.py passwordguesser -s {address} -p {port} -d --accounts-file accounts/accounts_multiple.txt', + 'python odat.py tnspoison -s {address} -p {port} -d --test-module' + ]) class OraclePatator(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Oracle Patator" - self.tags = ['default', 'databases'] + def __init__(self): + super().__init__() + self.name = "Oracle Patator" + self.tags = ['default', 'databases'] - def configure(self): - self.add_service_match('^oracle') + def configure(self): + self.match_service_name('^oracle') - def manual(self): - self.add_manual_command('Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:', 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000') + def manual(self): + self.add_manual_command('Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:', 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000') diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index f9725f8..8f07637 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -3,44 +3,44 @@ import os class QuickTCPPortScan(PortScan): - def __init__(self): - super().__init__() - self.name = "Top TCP Ports" - self.tags = ["default", "default-port-scan"] - self.priority = 0 + def __init__(self): + super().__init__() + self.name = "Top TCP Ports" + self.tags = ["default", "default-port-scan"] + self.priority = 0 - async def run(self, target): - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) - services = await target.extract_services(stdout) - await process.wait() - return services + async def run(self, target): + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + services = await target.extract_services(stdout) + await process.wait() + return services class AllTCPPortScan(PortScan): - def __init__(self): - super().__init__() - self.name = "All TCP Ports" - self.tags = ["default", "default-port-scan", "long"] + def __init__(self): + super().__init__() + self.name = "All TCP Ports" + self.tags = ["default", "default-port-scan", "long"] - async def run(self, target): - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) - services = await target.extract_services(stdout) - await process.wait() - return services + async def run(self, target): + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + services = await target.extract_services(stdout) + await process.wait() + return services class Top20UDPPortScan(PortScan): - def __init__(self): - super().__init__() - self.name = "Top 100 UDP Ports" - self.tags = ["default", "default-port-scan"] + def __init__(self): + super().__init__() + self.name = "Top 100 UDP Ports" + self.tags = ["default", "default-port-scan"] - async def run(self, target): - # Only run UDP scan if user is root. - if os.getuid() == 0: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all --top-ports 100 -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}', blocking=False) - services = await target.extract_services(stdout) - await process.wait() - return services - else: - error('UDP scan requires AutoRecon be run with root privileges.') + async def run(self, target): + # Only run UDP scan if user is root. + if os.getuid() == 0: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all --top-ports 100 -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}', blocking=False) + services = await target.extract_services(stdout) + await process.wait() + return services + else: + error('UDP scan requires AutoRecon be run with root privileges.') diff --git a/plugins/dns.py b/plugins/dns.py index df4565c..a84324a 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -2,13 +2,13 @@ from autorecon import ServiceScan class DNS(ServiceScan): - def __init__(self): - super().__init__() - self.name = "DNS" - self.tags = ['default', 'dns'] + def __init__(self): + super().__init__() + self.name = "DNS" + self.tags = ['default', 'dns'] - def configure(self): - self.add_service_match('^domain') + def configure(self): + self.match_service_name('^domain') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}') diff --git a/plugins/ftp.py b/plugins/ftp.py index 22c49be..04f8641 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -2,29 +2,29 @@ from autorecon import ServiceScan class NmapFTP(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'Nmap FTP' - self.tags = ['default', 'ftp'] + def __init__(self): + super().__init__() + self.name = 'Nmap FTP' + self.tags = ['default', 'ftp'] - def configure(self): - self.add_service_match(['^ftp', '^ftp\-data']) + def configure(self): + self.match_service_name(['^ftp', '^ftp\-data']) - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}') class BruteforceFTP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Bruteforce FTP" - self.tags = ['default', 'ftp'] + def __init__(self): + super().__init__() + self.name = "Bruteforce FTP" + self.tags = ['default', 'ftp'] - def configure(self): - self.add_service_match(['^ftp', '^ftp\-data']) + def configure(self): + self.match_service_name(['^ftp', '^ftp\-data']) - def manual(self): - self.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' - ]) + def manual(self): + self.add_manual_commands('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' + ]) diff --git a/plugins/http.py b/plugins/http.py index 4e3eddd..6ac62d2 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -4,183 +4,183 @@ import os class NmapHTTP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap HTTP" - self.tags = ['default', 'http'] + def __init__(self): + super().__init__() + self.name = "Nmap HTTP" + self.tags = ['default', 'http'] - def configure(self): - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) - self.add_pattern('Server: ([^\n]+)', description='Identified HTTP Server: {match}') - self.add_pattern('WebDAV is ENABLED', description='WebDAV is enabled') + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + self.add_pattern('Server: ([^\n]+)', description='Identified HTTP Server: {match}') + self.add_pattern('WebDAV is ENABLED', description='WebDAV is enabled') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(http* or ssl*) and not (brute or broadcast or dos or external or http-slowloris* or fuzzer)" -oN "{scandir}/{protocol}_{port}_{http_scheme}_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_{http_scheme}_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(http* or ssl*) and not (brute or broadcast or dos or external or http-slowloris* or fuzzer)" -oN "{scandir}/{protocol}_{port}_{http_scheme}_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_{http_scheme}_nmap.xml" {address}') class BruteforceHTTP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Bruteforce HTTP" - self.tags = ['default', 'http'] + def __init__(self): + super().__init__() + self.name = "Bruteforce HTTP" + self.tags = ['default', 'http'] - def configure(self): - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{address}/path/to/auth/area', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' - ]) + def manual(self): + self.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{address}/path/to/auth/area', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' + ]) class Curl(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Curl" - self.tags = ['default', 'http'] + def __init__(self): + super().__init__() + self.name = "Curl" + self.tags = ['default', 'http'] - def configure(self): - self.add_option("path", default="/", help="The path on the web server to curl. Default: %(default)s") - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) - self.add_pattern('(?i)Powered by [^\n]+') + def configure(self): + self.add_option("path", default="/", help="The path on the web server to curl. Default: %(default)s") + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + self.add_pattern('(?i)Powered by [^\n]+') - async def run(self, service): - if service.protocol == 'tcp': - await service.execute('curl -sSik {http_scheme}://{address}:{port}' + self.get_option('path'), outfile='{protocol}_{port}_{http_scheme}_curl.html') + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('curl -sSik {http_scheme}://{address}:{port}' + self.get_option('path'), outfile='{protocol}_{port}_{http_scheme}_curl.html') class CurlRobots(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Curl Robots" - self.tags = ['default', 'http'] + def __init__(self): + super().__init__() + self.name = "Curl Robots" + self.tags = ['default', 'http'] - def configure(self): - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) - async def run(self, service): - if service.protocol == 'tcp': - await service.execute('curl -sSik {http_scheme}://{address}:{port}/robots.txt', outfile='{protocol}_{port}_{http_scheme}_curl-robots.txt') + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('curl -sSik {http_scheme}://{address}:{port}/robots.txt', outfile='{protocol}_{port}_{http_scheme}_curl-robots.txt') class DirBuster(ServiceScan): - def __init__(self): - super().__init__() - self.name = "DirBuster" - self.slug = 'dirbuster' - self.priority = 0 - self.tags = ['default', 'http', 'long'] + def __init__(self): + super().__init__() + self.name = "DirBuster" + self.slug = 'dirbuster' + self.priority = 0 + self.tags = ['default', 'http', 'long'] - def configure(self): - self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') - self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist to use when directory busting. Specify the option multiple times to use multiple wordlists. Default: %(default)s') - self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) + def configure(self): + self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') + self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist to use when directory busting. Specify the option multiple times to use multiple wordlists. Default: %(default)s') + self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', - 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' - ]) + def manual(self): + self.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', + 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' + ]) - self.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' - ]) + self.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + ]) - self.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', - 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' - ]) + self.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', + 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' + ]) - self.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirb {http_scheme}://{address}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_big.txt"', - 'dirb {http_scheme}://{address}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' - ]) + self.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirb {http_scheme}://{address}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_big.txt"', + 'dirb {http_scheme}://{address}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' + ]) - self.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' - ]) + self.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + ]) - async def run(self, service): - for wordlist in self.get_option('wordlist'): - name = os.path.splitext(os.path.basename(wordlist))[0] - if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "txt,html,php,asp,aspx,jsp" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') - elif self.get_option('tool') == 'gobuster': - await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') - elif self.get_option('tool') == 'dirsearch': - await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w ' + wordlist + ' --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') - elif self.get_option('tool') == 'ffuf': - await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e ".txt,.html,.php,.asp,.aspx,.jsp" -v | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') - elif self.get_option('tool') == 'dirb': - await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') + async def run(self, service): + for wordlist in self.get_option('wordlist'): + name = os.path.splitext(os.path.basename(wordlist))[0] + if self.get_option('tool') == 'feroxbuster': + await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "txt,html,php,asp,aspx,jsp" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') + elif self.get_option('tool') == 'gobuster': + await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') + elif self.get_option('tool') == 'dirsearch': + await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w ' + wordlist + ' --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') + elif self.get_option('tool') == 'ffuf': + await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e ".txt,.html,.php,.asp,.aspx,.jsp" -v | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') + elif self.get_option('tool') == 'dirb': + await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') class Nikto(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'nikto' - self.tags = ['default', 'http', 'long'] + def __init__(self): + super().__init__() + self.name = 'nikto' + self.tags = ['default', 'http', 'long'] - def configure(self): - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') + def manual(self): + self.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') class WhatWeb(ServiceScan): - def __init__(self): - super().__init__() - self.name = "whatweb" - self.tags = ['default', 'http'] + def __init__(self): + super().__init__() + self.name = "whatweb" + self.tags = ['default', 'http'] - def configure(self): - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) - async def run(self, service): - if service.protocol == 'tcp': - await service.execute('whatweb --color=never --no-errors -a 3 -v {http_scheme}://{address}:{port} 2>&1', outfile='{protocol}_{port}_{http_scheme}_whatweb.txt') + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('whatweb --color=never --no-errors -a 3 -v {http_scheme}://{address}:{port} 2>&1', outfile='{protocol}_{port}_{http_scheme}_whatweb.txt') class WkHTMLToImage(ServiceScan): - def __init__(self): - super().__init__() - self.name = "wkhtmltoimage" - self.tags = ['default', 'http'] + def __init__(self): + super().__init__() + self.name = "wkhtmltoimage" + self.tags = ['default', 'http'] - def configure(self): - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) - async def run(self, service): - if which('wkhtmltoimage') is not None and service.protocol == 'tcp': - await service.execute('wkhtmltoimage --format png {http_scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') - else: - error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') + async def run(self, service): + if which('wkhtmltoimage') is not None and service.protocol == 'tcp': + await service.execute('wkhtmltoimage --format png {http_scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') + else: + error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') class WPScan(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'WPScan' - self.tags = ['default', 'http'] + def __init__(self): + super().__init__() + self.name = 'WPScan' + self.tags = ['default', 'http'] - def configure(self): - self.add_service_match('^http') - self.add_service_match('^nacn_http$', negative_match=True) + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') + def manual(self): + self.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') diff --git a/plugins/kerberos.py b/plugins/kerberos.py index b093bbc..5637f56 100644 --- a/plugins/kerberos.py +++ b/plugins/kerberos.py @@ -2,13 +2,13 @@ from autorecon import ServiceScan class NmapKerberos(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap Kerberos" - self.tags = ['default', 'kerberos', 'active-directory'] + def __init__(self): + super().__init__() + self.name = "Nmap Kerberos" + self.tags = ['default', 'kerberos', 'active-directory'] - def configure(self): - self.add_service_match(['^kerberos', '^kpasswd']) + def configure(self): + self.match_service_name(['^kerberos', '^kpasswd']) - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') diff --git a/plugins/ldap.py b/plugins/ldap.py index 39e88dd..2a08dca 100644 --- a/plugins/ldap.py +++ b/plugins/ldap.py @@ -2,28 +2,28 @@ from autorecon import ServiceScan class NmapLDAP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap LDAP" - self.tags = ['default', 'ldap', 'active-directory'] + def __init__(self): + super().__init__() + self.name = "Nmap LDAP" + self.tags = ['default', 'ldap', 'active-directory'] - def configure(self): - self.add_service_match('^ldap') + def configure(self): + self.match_service_name('^ldap') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ldap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ldap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ldap_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ldap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ldap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ldap_nmap.xml" {address}') class LDAPSearch(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'LDAP Search' - self.tags = ['default', 'ldap', 'active-directory'] + def __init__(self): + super().__init__() + self.name = 'LDAP Search' + self.tags = ['default', 'ldap', 'active-directory'] - def configure(self): - self.add_service_match('^ldap') + def configure(self): + self.match_service_name('^ldap') - def manual(self): - self.add_manual_command('ldapsearch command (modify before running):', [ - 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' - ]) + def manual(self): + self.add_manual_command('ldapsearch command (modify before running):', [ + 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' + ]) diff --git a/plugins/misc.py b/plugins/misc.py index 0e4144a..a682db5 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -2,169 +2,169 @@ from autorecon import ServiceScan class NmapCassandra(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap Cassandra" - self.tags = ['default', 'cassandra'] + def __init__(self): + super().__init__() + self.name = "Nmap Cassandra" + self.tags = ['default', 'cassandra'] - def configure(self): - self.add_service_match('^apani1') + def configure(self): + self.match_service_name('^apani1') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cassandra* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cassandra_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cassandra_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cassandra* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cassandra_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cassandra_nmap.xml" {address}') class NmapCUPS(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap CUPS" - self.tags = ['default', 'cups'] + def __init__(self): + super().__init__() + self.name = "Nmap CUPS" + self.tags = ['default', 'cups'] - def configure(self): - self.add_service_match('^ipp') + def configure(self): + self.match_service_name('^ipp') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cups* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cups_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cups_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cups* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cups_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cups_nmap.xml" {address}') class NmapDistccd(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap distccd" - self.tags = ['default', 'distccd'] + def __init__(self): + super().__init__() + self.name = "Nmap distccd" + self.tags = ['default', 'distccd'] - def configure(self): - self.add_service_match('^distccd') + def configure(self): + self.match_service_name('^distccd') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,distcc-cve2004-2687" --script-args="distcc-cve2004-2687.cmd=id" -oN "{scandir}/{protocol}_{port}_distcc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_distcc_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,distcc-cve2004-2687" --script-args="distcc-cve2004-2687.cmd=id" -oN "{scandir}/{protocol}_{port}_distcc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_distcc_nmap.xml" {address}') class NmapFinger(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap finger" - self.tags = ['default', 'finger'] + def __init__(self): + super().__init__() + self.name = "Nmap finger" + self.tags = ['default', 'finger'] - def configure(self): - self.add_service_match('^finger') + def configure(self): + self.match_service_name('^finger') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,finger" -oN "{scandir}/{protocol}_{port}_finger_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_finger_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,finger" -oN "{scandir}/{protocol}_{port}_finger_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_finger_nmap.xml" {address}') class NmapIMAP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap IMAP" - self.tags = ['default', 'imap', 'email'] + def __init__(self): + super().__init__() + self.name = "Nmap IMAP" + self.tags = ['default', 'imap', 'email'] - def configure(self): - self.add_service_match('^imap') + def configure(self): + self.match_service_name('^imap') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}') class NmapNNTP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap NNTP" - self.tags = ['default', 'nntp'] + def __init__(self): + super().__init__() + self.name = "Nmap NNTP" + self.tags = ['default', 'nntp'] - def configure(self): - self.add_service_match('^nntp') + def configure(self): + self.match_service_name('^nntp') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nntp-ntlm-info" -oN "{scandir}/{protocol}_{port}_nntp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nntp_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nntp-ntlm-info" -oN "{scandir}/{protocol}_{port}_nntp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nntp_nmap.xml" {address}') class NmapPOP3(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap POP3" - self.tags = ['default', 'pop3', 'email'] + def __init__(self): + super().__init__() + self.name = "Nmap POP3" + self.tags = ['default', 'pop3', 'email'] - def configure(self): - self.add_service_match('^pop3') + def configure(self): + self.match_service_name('^pop3') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(pop3* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_pop3_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_pop3_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(pop3* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_pop3_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_pop3_nmap.xml" {address}') class NmapRMI(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap RMI" - self.tags = ['default', 'rmi'] + def __init__(self): + super().__init__() + self.name = "Nmap RMI" + self.tags = ['default', 'rmi'] - def configure(self): - self.add_service_match(['^java\-rmi', '^rmiregistry']) + def configure(self): + self.match_service_name(['^java\-rmi', '^rmiregistry']) - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}') class NmapSMTP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap SMTP" - self.tags = ['default', 'smtp', 'email'] + def __init__(self): + super().__init__() + self.name = "Nmap SMTP" + self.tags = ['default', 'smtp', 'email'] - def configure(self): - self.add_service_match('^smtp') + def configure(self): + self.match_service_name('^smtp') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}') class SMTPUserEnum(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'SMTP-User-Enum' - self.tags = ['default', 'smtp', 'email'] + def __init__(self): + super().__init__() + self.name = 'SMTP-User-Enum' + self.tags = ['default', 'smtp', 'email'] - def configure(self): - self.add_service_match('^smtp') + def configure(self): + self.match_service_name('^smtp') - async def run(self, service): - await service.execute('smtp-user-enum -M VRFY -U "' + self.get_global('username_wordlist') + '" -t {address} -p {port} 2>&1', outfile='{protocol}_{port}_smtp_user-enum.txt') + async def run(self, service): + await service.execute('smtp-user-enum -M VRFY -U "' + self.get_global('username_wordlist') + '" -t {address} -p {port} 2>&1', outfile='{protocol}_{port}_smtp_user-enum.txt') class NmapTelnet(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'Nmap Telnet' - self.tags = ['default', 'telnet'] + def __init__(self): + super().__init__() + self.name = 'Nmap Telnet' + self.tags = ['default', 'telnet'] - def configure(self): - self.add_service_match('^telnet') + def configure(self): + self.match_service_name('^telnet') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,telnet-encryption,telnet-ntlm-info" -oN "{scandir}/{protocol}_{port}_telnet-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_telnet_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,telnet-encryption,telnet-ntlm-info" -oN "{scandir}/{protocol}_{port}_telnet-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_telnet_nmap.xml" {address}') class NmapTFTP(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'Nmap TFTP' - self.tags = ['default', 'tftp'] + def __init__(self): + super().__init__() + self.name = 'Nmap TFTP' + self.tags = ['default', 'tftp'] - def configure(self): - self.add_service_match('^tftp') + def configure(self): + self.match_service_name('^tftp') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,tftp-enum" -oN "{scandir}/{protocol}_{port}_tftp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_tftp_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,tftp-enum" -oN "{scandir}/{protocol}_{port}_tftp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_tftp_nmap.xml" {address}') class NmapVNC(ServiceScan): - def __init__(self): - super().__init__() - self.name = 'Nmap VNC' - self.tags = ['default', 'vnc'] + def __init__(self): + super().__init__() + self.name = 'Nmap VNC' + self.tags = ['default', 'vnc'] - def configure(self): - self.add_service_match('^vnc') + def configure(self): + self.match_service_name('^vnc') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}') diff --git a/plugins/nfs.py b/plugins/nfs.py index 8210580..d47df84 100644 --- a/plugins/nfs.py +++ b/plugins/nfs.py @@ -2,26 +2,26 @@ from autorecon import ServiceScan class NmapNFS(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap NFS" - self.tags = ['default', 'nfs'] + def __init__(self): + super().__init__() + self.name = "Nmap NFS" + self.tags = ['default', 'nfs'] - def configure(self): - self.add_service_match(['^nfs', '^rpcbind']) + def configure(self): + self.match_service_name(['^nfs', '^rpcbind']) - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rpcinfo or nfs*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_nfs_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nfs_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rpcinfo or nfs*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_nfs_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nfs_nmap.xml" {address}') class Showmount(ServiceScan): - def __init__(self): - super().__init__() - self.name = "showmount" - self.tags = ['default', 'nfs'] + def __init__(self): + super().__init__() + self.name = "showmount" + self.tags = ['default', 'nfs'] - def configure(self): - self.add_service_match(['^nfs', '^rpcbind']) + def configure(self): + self.match_service_name(['^nfs', '^rpcbind']) - async def run(self, service): - await service.execute('showmount -e {address} 2>&1', outfile='{protocol}_{port}_showmount.txt') + async def run(self, service): + await service.execute('showmount -e {address} 2>&1', outfile='{protocol}_{port}_showmount.txt') diff --git a/plugins/rdp.py b/plugins/rdp.py index a4dc5cb..c2be577 100644 --- a/plugins/rdp.py +++ b/plugins/rdp.py @@ -2,29 +2,29 @@ from autorecon import ServiceScan class NmapRDP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap RDP" - self.tags = ['default', 'rdp'] + def __init__(self): + super().__init__() + self.name = "Nmap RDP" + self.tags = ['default', 'rdp'] - def configure(self): - self.add_service_match(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) + def configure(self): + self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rdp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rdp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rdp_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rdp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rdp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rdp_nmap.xml" {address}') class BruteforceRDP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Bruteforce RDP" - self.tags = ['default', 'rdp'] + def __init__(self): + super().__init__() + self.name = "Bruteforce RDP" + self.tags = ['default', 'rdp'] - def configure(self): - self.add_service_match(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) + def configure(self): + self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) - def manual(self): - self.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' - ]) + def manual(self): + self.add_manual_commands('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' + ]) diff --git a/plugins/rpc.py b/plugins/rpc.py index 63535dd..be5beb9 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -2,26 +2,26 @@ from autorecon import ServiceScan class NmapMSRPC(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap MSRPC" - self.tags = ['default', 'rpc'] + def __init__(self): + super().__init__() + self.name = "Nmap MSRPC" + self.tags = ['default', 'rpc'] - def configure(self): - self.add_service_match(['^msrpc', '^rpcbind', '^erpc']) + def configure(self): + self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,msrpc-enum,rpc-grind,rpcinfo" -oN "{scandir}/{protocol}_{port}_rpc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rpc_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,msrpc-enum,rpc-grind,rpcinfo" -oN "{scandir}/{protocol}_{port}_rpc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rpc_nmap.xml" {address}') class RPCClient(ServiceScan): - def __init__(self): - super().__init__() - self.name = "rpcclient" - self.tags = ['default', 'rpc'] + def __init__(self): + super().__init__() + self.name = "rpcclient" + self.tags = ['default', 'rpc'] - def configure(self): - self.add_service_match(['^msrpc', '^rpcbind', '^erpc']) + def configure(self): + self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) - def manual(self): - self.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') + def manual(self): + self.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') diff --git a/plugins/sip.py b/plugins/sip.py index da93233..7553de0 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -2,26 +2,26 @@ from autorecon import ServiceScan class NmapSIP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap SIP" - self.tags = ['default', 'sip'] + def __init__(self): + super().__init__() + self.name = "Nmap SIP" + self.tags = ['default', 'sip'] - def configure(self): - self.add_service_match('^asterisk') + def configure(self): + self.match_service_name('^asterisk') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,sip-enum-users,sip-methods" -oN "{scandir}/{protocol}_{port}_sip_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_sip_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,sip-enum-users,sip-methods" -oN "{scandir}/{protocol}_{port}_sip_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_sip_nmap.xml" {address}') class SIPVicious(ServiceScan): - def __init__(self): - super().__init__() - self.name = "SIPVicious" - self.tags = ['default', 'sip'] + def __init__(self): + super().__init__() + self.name = "SIPVicious" + self.tags = ['default', 'sip'] - def configure(self): - self.add_service_match('^asterisk') + def configure(self): + self.match_service_name('^asterisk') - def manual(self): - self.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') + def manual(self): + self.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') diff --git a/plugins/smb.py b/plugins/smb.py index 40b84a2..e5bd73e 100644 --- a/plugins/smb.py +++ b/plugins/smb.py @@ -2,84 +2,84 @@ from autorecon import ServiceScan class NmapSMB(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap SMB" - self.tags = ['default', 'smb', 'active-directory'] + def __init__(self): + super().__init__() + self.name = "Nmap SMB" + self.tags = ['default', 'smb', 'active-directory'] - def configure(self): - self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - def manual(self): - self.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' - ]) + def manual(self): + self.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' + ]) - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}') class Enum4Linux(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Enum4Linux" - self.tags = ['default', 'enum4linux', 'active-directory'] + def __init__(self): + super().__init__() + self.name = "Enum4Linux" + self.tags = ['default', 'enum4linux', 'active-directory'] - def configure(self): - self.add_service_match(['^ldap', '^smb', '^microsoft\-ds', '^netbios']) - self.add_port_match('tcp', [139, 389, 445]) - self.add_port_match('udp', 137) - self.run_once(True) + def configure(self): + self.match_service_name(['^ldap', '^smb', '^microsoft\-ds', '^netbios']) + self.match_port('tcp', [139, 389, 445]) + self.match_port('udp', 137) + self.run_once(True) - async def run(self, service): - await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') + async def run(self, service): + await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') class NBTScan(ServiceScan): - def __init__(self): - super().__init__() - self.name = "nbtscan" - self.tags = ['default', 'netbios', 'active-directory'] + def __init__(self): + super().__init__() + self.name = "nbtscan" + self.tags = ['default', 'netbios', 'active-directory'] - def configure(self): - self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) - self.add_port_match('udp', 137) - self.run_once(True) + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + self.match_port('udp', 137) + self.run_once(True) - async def run(self, service): - await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') + async def run(self, service): + await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') class SMBClient(ServiceScan): - def __init__(self): - super().__init__() - self.name = "SMBClient" - self.tags = ['default', 'smb', 'active-directory'] + def __init__(self): + super().__init__() + self.name = "SMBClient" + self.tags = ['default', 'smb', 'active-directory'] - def configure(self): - self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) - self.add_port_match('tcp', [139, 445]) - self.run_once(True) + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + self.match_port('tcp', [139, 445]) + self.run_once(True) - async def run(self, service): - await service.execute('smbclient -L\\\\ -N -I {address} 2>&1', outfile='smbclient.txt') + async def run(self, service): + await service.execute('smbclient -L\\\\ -N -I {address} 2>&1', outfile='smbclient.txt') class SMBMap(ServiceScan): - def __init__(self): - super().__init__() - self.name = "SMBMap" - self.tags = ['default', 'smb', 'active-directory'] + def __init__(self): + super().__init__() + self.name = "SMBMap" + self.tags = ['default', 'smb', 'active-directory'] - def configure(self): - self.add_service_match(['^smb', '^microsoft\-ds', '^netbios']) + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - async def run(self, service): - await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') - await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') - await service.execute('smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') + async def run(self, service): + await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') diff --git a/plugins/snmp.py b/plugins/snmp.py index 83d552f..05b63ce 100644 --- a/plugins/snmp.py +++ b/plugins/snmp.py @@ -2,51 +2,51 @@ from autorecon import ServiceScan class NmapSNMP(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap SNMP" - self.tags = ['default', 'snmp'] + def __init__(self): + super().__init__() + self.name = "Nmap SNMP" + self.tags = ['default', 'snmp'] - def configure(self): - self.add_service_match('^snmp') + def configure(self): + self.match_service_name('^snmp') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(snmp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_snmp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_snmp_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(snmp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_snmp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_snmp_nmap.xml" {address}') class OneSixtyOne(ServiceScan): - def __init__(self): - super().__init__() - self.name = "OneSixtyOne" - self.tags = ['default', 'snmp'] + def __init__(self): + super().__init__() + self.name = "OneSixtyOne" + self.tags = ['default', 'snmp'] - def configure(self): - self.add_service_match('^snmp') - self.add_port_match('udp', 161) - self.run_once(True) - self.add_option('community-strings', default='/usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt', help='The file containing a list of community strings to try. Default: %(default)s') + def configure(self): + self.match_service_name('^snmp') + self.match_port('udp', 161) + self.run_once(True) + self.add_option('community-strings', default='/usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt', help='The file containing a list of community strings to try. Default: %(default)s') - async def run(self, service): - await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') + async def run(self, service): + await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') class SNMPWalk(ServiceScan): - def __init__(self): - super().__init__() - self.name = "SNMPWalk" - self.tags = ['default', 'snmp'] + def __init__(self): + super().__init__() + self.name = "SNMPWalk" + self.tags = ['default', 'snmp'] - def configure(self): - self.add_service_match('^snmp') - self.add_port_match('udp', 161) - self.run_once(True) + def configure(self): + self.match_service_name('^snmp') + self.match_port('udp', 161) + self.run_once(True) - async def run(self, service): - await service.execute('snmpwalk -c public -v 1 {address} 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.1.6.0 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_system_processes.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.2 2>&1', outfile='{scandir}/{protocol}_{port}_snmp_snmpwalk_running_processes.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_process_paths.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_storage_units.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_software_names.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.4.1.77.1.2.25 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_user_accounts.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.6.13.1.3 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_tcp_ports.txt') + async def run(self, service): + await service.execute('snmpwalk -c public -v 1 {address} 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.1.6.0 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_system_processes.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.2 2>&1', outfile='{scandir}/{protocol}_{port}_snmp_snmpwalk_running_processes.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_process_paths.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_storage_units.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_software_names.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.4.1.77.1.2.25 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_user_accounts.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.6.13.1.3 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_tcp_ports.txt') diff --git a/plugins/ssh.py b/plugins/ssh.py index 637c390..07e0d97 100644 --- a/plugins/ssh.py +++ b/plugins/ssh.py @@ -2,29 +2,29 @@ from autorecon import ServiceScan class NmapSSH(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Nmap SSH" - self.tags = ['default', 'ssh'] + def __init__(self): + super().__init__() + self.name = "Nmap SSH" + self.tags = ['default', 'ssh'] - def configure(self): - self.add_service_match('^ssh') + def configure(self): + self.match_service_name('^ssh') - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,ssh2-enum-algos,ssh-hostkey,ssh-auth-methods" -oN "{scandir}/{protocol}_{port}_ssh_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ssh_nmap.xml" {address}') + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,ssh2-enum-algos,ssh-hostkey,ssh-auth-methods" -oN "{scandir}/{protocol}_{port}_ssh_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ssh_nmap.xml" {address}') class BruteforceSSH(ServiceScan): - def __init__(self): - super().__init__() - self.name = "Bruteforce SSH" - self.tags = ['default', 'ssh'] + def __init__(self): + super().__init__() + self.name = "Bruteforce SSH" + self.tags = ['default', 'ssh'] - def configure(self): - self.add_service_match('ssh') + def configure(self): + self.match_service_name('ssh') - def manual(self): - self.add_manual_command('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' - ]) + def manual(self): + self.add_manual_command('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', + 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' + ]) diff --git a/plugins/sslscan.py b/plugins/sslscan.py index c9a8833..1ece982 100644 --- a/plugins/sslscan.py +++ b/plugins/sslscan.py @@ -2,15 +2,15 @@ from autorecon import ServiceScan class SSLScan(ServiceScan): - def __init__(self): - super().__init__() - self.name = "SSL Scan" - self.tags = ['default', 'ssl', 'tls'] + def __init__(self): + super().__init__() + self.name = "SSL Scan" + self.tags = ['default', 'ssl', 'tls'] - def configure(self): - self.add_service_match('.+') - self.require_ssl(True) + def configure(self): + self.match_service_name('.+') + self.require_ssl(True) - async def run(self, service): - if service.protocol == 'tcp' and service.secure: - await service.execute('sslscan --show-certificate --no-colour {address}:{port} 2>&1', outfile='{protocol}_{port}_sslscan.html') + async def run(self, service): + if service.protocol == 'tcp' and service.secure: + await service.execute('sslscan --show-certificate --no-colour {address}:{port} 2>&1', outfile='{protocol}_{port}_sslscan.html') From 8f34de0a89727fb071d4bbebb2f91bb690e3bfff Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 2 Aug 2021 19:35:49 -0400 Subject: [PATCH 07/95] Fixed bug in manual commands function. The manual commands function will now merge command arrays instead of replacing them, while removing duplicate commands and preserving order. Added a default option to get_global / get_global_option function in case user deletes global options. Updated several plugins that use global options. --- autorecon.py | 14 ++++++++++---- plugins/ftp.py | 4 ++-- plugins/http.py | 24 ++++++++++++------------ plugins/rdp.py | 4 ++-- plugins/ssh.py | 4 ++-- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/autorecon.py b/autorecon.py index 49950d1..4feb510 100644 --- a/autorecon.py +++ b/autorecon.py @@ -245,23 +245,29 @@ class Plugin(object): return None @final - def get_global_option(self, name): + def get_global_option(self, name, default=None): name = 'global.' + slugify(name).replace('-', '_') if name in vars(self.autorecon.args): return vars(self.autorecon.args)[name] else: + if default: + return default return None @final - def get_global(self, name): - return self.get_global_option(name) + def get_global(self, name, default=None): + return self.get_global_option(name, default) @final def add_manual_commands(self, description, commands): if not isinstance(commands, list): commands = [commands] - self.manual_commands[description] = commands + if description not in self.manual_commands: + self.manual_commands[description] = [] + + # Merge in new unique commands, while preserving order. + [self.manual_commands[description].append(m) for m in commands if m not in self.manual_commands[description]] @final def add_manual_command(self, description, command): diff --git a/plugins/ftp.py b/plugins/ftp.py index 04f8641..456c44e 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -25,6 +25,6 @@ class BruteforceFTP(ServiceScan): def manual(self): self.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' ]) diff --git a/plugins/http.py b/plugins/http.py index 6ac62d2..7a3321b 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -31,10 +31,10 @@ class BruteforceHTTP(ServiceScan): def manual(self): self.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{address}/path/to/auth/area', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{address}/path/to/auth/area', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' ]) class Curl(ServiceScan): @@ -87,18 +87,18 @@ class DirBuster(ServiceScan): def manual(self): self.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', - 'feroxbuster -u {http_scheme}://{address}:{port} -t 10 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' + 'feroxbuster -u {http_scheme}://{address}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', + 'feroxbuster -u {http_scheme}://{address}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' ]) self.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster dir -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) self.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', - 'dirsearch -u {http_scheme}://{address}:{port}/ -t 16 -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' + 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', + 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' ]) self.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ @@ -107,8 +107,8 @@ class DirBuster(ServiceScan): ]) self.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster -u {http_scheme}://{address}:{port}/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) async def run(self, service): diff --git a/plugins/rdp.py b/plugins/rdp.py index c2be577..f1dce27 100644 --- a/plugins/rdp.py +++ b/plugins/rdp.py @@ -25,6 +25,6 @@ class BruteforceRDP(ServiceScan): def manual(self): self.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' ]) diff --git a/plugins/ssh.py b/plugins/ssh.py index 07e0d97..d57d30e 100644 --- a/plugins/ssh.py +++ b/plugins/ssh.py @@ -25,6 +25,6 @@ class BruteforceSSH(ServiceScan): def manual(self): self.add_manual_command('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', - 'medusa -U "' + self.get_global('username_wordlist') + '" -P "' + self.get_global('password_wordlist') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' ]) From 3d67b185ea0e874ca58cdb30b9de941dca339080 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 2 Aug 2021 23:14:42 -0400 Subject: [PATCH 08/95] Updated SMTP user enumeration. Replaced smtp-user-enum with hydra. --- README.md | 3 +-- plugins/misc.py | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9945c62..84cf6e0 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,6 @@ onesixtyone oscanner smbclient smbmap -smtp-user-enum snmpwalk sslscan svwar @@ -69,7 +68,7 @@ wkhtmltopdf On Kali Linux, you can ensure these are all installed using the following command: ```bash -$ sudo apt install seclists curl enum4linux feroxbuster nbtscan nikto nmap onesixtyone oscanner smbclient smbmap smtp-user-enum snmp sslscan sipvicious tnscmd10g whatweb wkhtmltopdf +$ sudo apt install seclists curl enum4linux feroxbuster nbtscan nikto nmap onesixtyone oscanner smbclient smbmap snmp sslscan sipvicious tnscmd10g whatweb wkhtmltopdf ``` ## Installation diff --git a/plugins/misc.py b/plugins/misc.py index a682db5..adf570e 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -128,7 +128,14 @@ class SMTPUserEnum(ServiceScan): self.match_service_name('^smtp') async def run(self, service): - await service.execute('smtp-user-enum -M VRFY -U "' + self.get_global('username_wordlist') + '" -t {address} -p {port} 2>&1', outfile='{protocol}_{port}_smtp_user-enum.txt') + await service.execute('hydra smtp-enum://{address}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') + await service.execute('hydra smtp-enum://{address}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') + + def manual(self): + self.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ + 'hydra smtp-enum://{address}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' + ]) + class NmapTelnet(ServiceScan): From 66c17c841ec9487483a8aa737715d6c117a5363e Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 2 Aug 2021 23:22:10 -0400 Subject: [PATCH 09/95] Update README.md Spelling correction. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84cf6e0..e162a36 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ The scans/xml directory stores any XML output (e.g. from Nmap scans) separately > >\- d0hnuts (rooted 5/5 exam hosts) -> Autorecon is not just any other tool, it is a recon correlation framwork for engagements. This helped me fire a whole bunch of scans while I was working on other targets. This can help a lot in time management. This assisted me to own 4/5 boxes in pwk exam! Result: Passed! +> Autorecon is not just any other tool, it is a recon correlation framweork for engagements. This helped me fire a whole bunch of scans while I was working on other targets. This can help a lot in time management. This assisted me to own 4/5 boxes in pwk exam! Result: Passed! > >\- Wh0ami (rooted 4/5 exam hosts) From d0aef68ab25ff2978c57c0d14638349296bfe7eb Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 3 Aug 2021 00:08:40 -0400 Subject: [PATCH 10/95] Update autorecon.py Fixes #68 Instead of messing around with limits, we should ignore any line longer than 64 KiB, as it is likely invalid data of some kind anyway. --- autorecon.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/autorecon.py b/autorecon.py index 4feb510..7851036 100644 --- a/autorecon.py +++ b/autorecon.py @@ -161,7 +161,12 @@ class CommandStreamReader(object): while True: if self.stream.at_eof(): break - line = (await self.stream.readline()).decode('utf8').rstrip() + try: + line = (await self.stream.readline()).decode('utf8').rstrip() + except ValueError: + error('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} A line was longer than 64 KiB and cannot be processed. Ignoring.') + continue + if self.target.autorecon.config['verbose'] >= 2: if line != '': info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} ' + line.replace('{', '{{').replace('}', '}}')) From 5e7b2b4c1942508123c5fc540082960b3fa375d6 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 13 Aug 2021 17:18:11 -0400 Subject: [PATCH 11/95] Update autorecon.py Added --version --- autorecon.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/autorecon.py b/autorecon.py index 7851036..43aebfd 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1030,6 +1030,7 @@ async def main(): parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running. Default: false') parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders.') parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') + parser.add_argument('--version', action='store_true', help='Prints the AutoRecon version and exits.') parser.error = lambda s: fail(s[0].upper() + s[1:]) args, unknown = parser.parse_known_args() @@ -1037,6 +1038,10 @@ async def main(): autorecon.argparse = parser + if args.version: + print('AutoRecon v2.0-beta1') + sys.exit(0) + # Parse config file and args for global.toml first. if not os.path.isfile(args.config_file): fail('Error: Specified config file "' + args.config_file + '" does not exist.') From 7fa825c73334ceee24a45080f3844268a93894f7 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 13 Aug 2021 17:49:01 -0400 Subject: [PATCH 12/95] Update autorecon.py Added feature to collect services which AutoRecon "missed" (i.e. doesn't have plugins for) and report them at the end. --- autorecon.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/autorecon.py b/autorecon.py index 43aebfd..e256a72 100644 --- a/autorecon.py +++ b/autorecon.py @@ -164,12 +164,12 @@ class CommandStreamReader(object): try: line = (await self.stream.readline()).decode('utf8').rstrip() except ValueError: - error('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} A line was longer than 64 KiB and cannot be processed. Ignoring.') + error('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} A line was longer than 64 KiB and cannot be processed. Ignoring.') continue if self.target.autorecon.config['verbose'] >= 2: if line != '': - info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}]{crst} ' + line.replace('{', '{{').replace('}', '}}')) + info('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} ' + line.replace('{', '{{').replace('}', '}}')) for p in self.patterns: matches = p.pattern.findall(line) for match in matches: @@ -177,11 +177,11 @@ class CommandStreamReader(object): with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file: if p.description: if self.target.autorecon.config['verbose'] >= 1: - info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}' + p.description.replace('{match}', '{bblue}' + match + '{crst}{bmagenta}') + '{rst}') + info('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} {bmagenta}' + p.description.replace('{match}', '{bblue}' + match + '{crst}{bmagenta}') + '{rst}') file.writelines(p.description.replace('{match}', match) + '\n\n') else: if self.target.autorecon.config['verbose'] >= 1: - info('{blue}[{bright}' + self.target.address + '/' + self.tag + '{srst}] {crst}{bmagenta}Matched Pattern: {bblue}' + match + '{rst}') + info('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} {bmagenta}Matched Pattern: {bblue}' + match + '{rst}') file.writelines('Matched Pattern: ' + match + '\n\n') if self.outfile is not None: @@ -367,6 +367,7 @@ class AutoRecon(object): self.argparse = None self.argparse_group = None self.args = None + self.missing_services = [] self.tags = [] self.excluded_tags = [] self.patterns = [] @@ -902,6 +903,7 @@ async def scan_target(target): if protocol == 'udp': nmap_extra += ' -sU' + service_match = False matching_plugins = [] heading = False @@ -910,6 +912,7 @@ async def scan_target(target): for s in plugin.service_names: if re.search(s, service.name): + service_match = True plugin_tag_set = set(plugin.tags) matching_tags = False @@ -934,7 +937,7 @@ async def scan_target(target): if plugin_is_runnable and matching_tags and not excluded_tags: # Skip plugin if run_once_boolean and plugin already in target scans if plugin.run_once_boolean and (plugin.slug,) in target.scans: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') continue # Skip plugin if require_ssl_boolean and port is not secure @@ -943,17 +946,17 @@ async def scan_target(target): # Skip plugin if service port is in ignore_ports: if port in plugin.ignore_ports[protocol]: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') continue # Skip plugin if plugin has required ports and service port is not in them: if plugin.ports[protocol] and port not in plugin.ports[protocol]: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin can only run on specific ports. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}') continue for i in plugin.ignore_service_names: if re.search(i, service.name): - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin cannot be run against this service. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against this service. Skipping.{rst}') continue # TODO: check if plugin matches tags, BUT run manual commands anyway! @@ -980,12 +983,18 @@ async def scan_target(target): scan_tuple = (plugin.slug,) if scan_tuple in target.scans: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + '{srst}] Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') continue else: target.scans.append(scan_tuple) pending.add(asyncio.create_task(service_scan(plugin, service))) + + if not service_match: + warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins.{rst}') + if service.full_tag() not in target.autorecon.missing_services: + target.autorecon.missing_services.append(service.full_tag()) + heartbeat.cancel() elapsed_time = calculate_elapsed_time(start_time) @@ -1396,7 +1405,6 @@ async def main(): elapsed_time = calculate_elapsed_time(start_time) warn('{byellow}AutoRecon took longer than the specified timeout period (' + str(autorecon.config['timeout']) + ' min). Cancelling all scans and exiting.{rst}') - sys.exit(0) else: while len(asyncio.all_tasks()) > 1: # this code runs in the main() task so it will be the only task left running await asyncio.sleep(1) @@ -1404,6 +1412,9 @@ async def main(): elapsed_time = calculate_elapsed_time(start_time) info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}') + if autorecon.missing_services: + warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') + if __name__ == '__main__': signal.signal(signal.SIGINT, cancel_all_tasks) try: From 838137c6bcbcba78f2891fdd9a07f9f1fd665c78 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:06:09 -0400 Subject: [PATCH 13/95] Update autorecon.py Quick bug fix for previous update. --- autorecon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/autorecon.py b/autorecon.py index e256a72..9eb3027 100644 --- a/autorecon.py +++ b/autorecon.py @@ -942,15 +942,18 @@ async def scan_target(target): # Skip plugin if require_ssl_boolean and port is not secure if plugin.require_ssl_boolean and not service.secure: + service_match = False continue # Skip plugin if service port is in ignore_ports: if port in plugin.ignore_ports[protocol]: + service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') continue # Skip plugin if plugin has required ports and service port is not in them: if plugin.ports[protocol] and port not in plugin.ports[protocol]: + service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}') continue From f07f4b6a3f47793a9826a65fd640b337dc7374ea Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:21:25 -0400 Subject: [PATCH 14/95] Update autorecon.py Removing missed service collection for now. Too many bugs. --- autorecon.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/autorecon.py b/autorecon.py index 9eb3027..2f269e4 100644 --- a/autorecon.py +++ b/autorecon.py @@ -912,7 +912,6 @@ async def scan_target(target): for s in plugin.service_names: if re.search(s, service.name): - service_match = True plugin_tag_set = set(plugin.tags) matching_tags = False @@ -942,18 +941,15 @@ async def scan_target(target): # Skip plugin if require_ssl_boolean and port is not secure if plugin.require_ssl_boolean and not service.secure: - service_match = False continue # Skip plugin if service port is in ignore_ports: if port in plugin.ignore_ports[protocol]: - service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') continue # Skip plugin if plugin has required ports and service port is not in them: if plugin.ports[protocol] and port not in plugin.ports[protocol]: - service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}') continue @@ -993,10 +989,10 @@ async def scan_target(target): pending.add(asyncio.create_task(service_scan(plugin, service))) - if not service_match: - warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins.{rst}') - if service.full_tag() not in target.autorecon.missing_services: - target.autorecon.missing_services.append(service.full_tag()) + #if not service_match: + # warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins.{rst}') + # if service.full_tag() not in target.autorecon.missing_services: + # target.autorecon.missing_services.append(service.full_tag()) heartbeat.cancel() elapsed_time = calculate_elapsed_time(start_time) @@ -1415,8 +1411,8 @@ async def main(): elapsed_time = calculate_elapsed_time(start_time) info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}') - if autorecon.missing_services: - warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') + #if autorecon.missing_services: + # warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') if __name__ == '__main__': signal.signal(signal.SIGINT, cancel_all_tasks) From 5740efc86c72f999dcf7438d6ddbb0e5f7c93198 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:53:59 -0400 Subject: [PATCH 15/95] Missed Services Collection Added feature to collect services which AutoRecon "missed" (i.e. doesn't have plugins for) and report them at the end. --- autorecon.py | 27 +++++++++++++++++++++------ plugins/sslscan.py | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/autorecon.py b/autorecon.py index 2f269e4..485c831 100644 --- a/autorecon.py +++ b/autorecon.py @@ -305,6 +305,7 @@ class ServiceScan(Plugin): self.ignore_ports = {'tcp':[], 'udp':[]} self.service_names = [] self.ignore_service_names = [] + self.match_all_service_names_boolean = False self.run_once_boolean = False self.require_ssl_boolean = False @@ -354,6 +355,10 @@ class ServiceScan(Plugin): def run_once(self, boolean): self.run_once_boolean = boolean + @final + def match_all_service_names(self, boolean): + self.match_all_service_names_boolean = boolean + class AutoRecon(object): def __init__(self): @@ -908,10 +913,14 @@ async def scan_target(target): heading = False for plugin in target.autorecon.plugin_types['service']: + plugin_service_match = False plugin_tag = service.tag() + '/' + plugin.slug for s in plugin.service_names: if re.search(s, service.name): + plugin_service_match = True + + if plugin.match_all_service_names_boolean or plugin_service_match: plugin_tag_set = set(plugin.tags) matching_tags = False @@ -941,15 +950,18 @@ async def scan_target(target): # Skip plugin if require_ssl_boolean and port is not secure if plugin.require_ssl_boolean and not service.secure: + plugin_service_match = False continue # Skip plugin if service port is in ignore_ports: if port in plugin.ignore_ports[protocol]: + plugin_service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') continue # Skip plugin if plugin has required ports and service port is not in them: if plugin.ports[protocol] and port not in plugin.ports[protocol]: + plugin_service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}') continue @@ -974,6 +986,9 @@ async def scan_target(target): break + if plugin_service_match: + service_match = True + for plugin in matching_plugins: plugin_tag = service.tag() + '/' + plugin.slug @@ -989,10 +1004,10 @@ async def scan_target(target): pending.add(asyncio.create_task(service_scan(plugin, service))) - #if not service_match: - # warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins.{rst}') - # if service.full_tag() not in target.autorecon.missing_services: - # target.autorecon.missing_services.append(service.full_tag()) + if not service_match: + warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins based on the service name.{rst}') + if service.full_tag() not in target.autorecon.missing_services: + target.autorecon.missing_services.append(service.full_tag()) heartbeat.cancel() elapsed_time = calculate_elapsed_time(start_time) @@ -1411,8 +1426,8 @@ async def main(): elapsed_time = calculate_elapsed_time(start_time) info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}') - #if autorecon.missing_services: - # warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') + if autorecon.missing_services: + warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins based on the service name. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') if __name__ == '__main__': signal.signal(signal.SIGINT, cancel_all_tasks) diff --git a/plugins/sslscan.py b/plugins/sslscan.py index 1ece982..4730238 100644 --- a/plugins/sslscan.py +++ b/plugins/sslscan.py @@ -8,7 +8,7 @@ class SSLScan(ServiceScan): self.tags = ['default', 'ssl', 'tls'] def configure(self): - self.match_service_name('.+') + self.match_all_service_names(True) self.require_ssl(True) async def run(self, service): From 40dcea93b7e6e1c372d8b7c6336446aa910d08a7 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 14 Aug 2021 19:41:10 -0400 Subject: [PATCH 16/95] Updated Manual Commands Manual commands function manual() now requires two arguments to make it more advanced. --- autorecon.py | 69 ++++++++++++++++++++++++-------------------- plugins/databases.py | 20 ++++++------- plugins/ftp.py | 4 +-- plugins/http.py | 24 +++++++-------- plugins/ldap.py | 4 +-- plugins/misc.py | 4 +-- plugins/rdp.py | 4 +-- plugins/rpc.py | 4 +-- plugins/sip.py | 4 +-- plugins/smb.py | 4 +-- plugins/ssh.py | 4 +-- 11 files changed, 76 insertions(+), 69 deletions(-) diff --git a/autorecon.py b/autorecon.py index 485c831..0bec848 100644 --- a/autorecon.py +++ b/autorecon.py @@ -86,6 +86,7 @@ class Service: self.port = int(port) self.name = name self.secure = secure + self.manual_commands = {} @final def tag(self): @@ -95,6 +96,20 @@ class Service: def full_tag(self): return self.protocol + '/' + str(self.port) + '/' + self.name + '/' + ('secure' if self.secure else 'insecure') + @final + def add_manual_commands(self, description, commands): + if not isinstance(commands, list): + commands = [commands] + if description not in self.manual_commands: + self.manual_commands[description] = [] + + # Merge in new unique commands, while preserving order. + [self.manual_commands[description].append(m) for m in commands if m not in self.manual_commands[description]] + + @final + def add_manual_command(self, description, command): + self.add_manual_commands(description, command) + @final async def execute(self, cmd, blocking=True, outfile=None, errfile=None): target = self.target @@ -209,7 +224,6 @@ class Plugin(object): self.tags = ['default'] self.priority = 1 self.patterns = [] - self.manual_commands = {} self.autorecon = None self.disabled = False @@ -264,20 +278,6 @@ class Plugin(object): def get_global(self, name, default=None): return self.get_global_option(name, default) - @final - def add_manual_commands(self, description, commands): - if not isinstance(commands, list): - commands = [commands] - if description not in self.manual_commands: - self.manual_commands[description] = [] - - # Merge in new unique commands, while preserving order. - [self.manual_commands[description].append(m) for m in commands if m not in self.manual_commands[description]] - - @final - def add_manual_command(self, description, command): - self.add_manual_commands(description, command) - @final def add_pattern(self, pattern, description=None): try: @@ -470,8 +470,12 @@ class AutoRecon(object): if member_name == 'configure': configure_function_found = True elif member_name == 'run' and inspect.iscoroutinefunction(member_value): + if len(inspect.signature(member_value).parameters) != 2: + fail('Error: the "run" coroutine in the plugin "' + plugin.name + '" should have two arguments.', file=sys.stderr) run_coroutine_found = True elif member_name == 'manual': + if len(inspect.signature(member_value).parameters) != 3: + fail('Error: the "manual" function in the plugin "' + plugin.name + '" should have three arguments.', file=sys.stderr) manual_function_found = True if not run_coroutine_found and not manual_function_found: @@ -913,6 +917,7 @@ async def scan_target(target): heading = False for plugin in target.autorecon.plugin_types['service']: + plugin_was_run = False plugin_service_match = False plugin_tag = service.tag() + '/' + plugin.slug @@ -971,18 +976,26 @@ async def scan_target(target): continue # TODO: check if plugin matches tags, BUT run manual commands anyway! + plugin_was_run = True matching_plugins.append(plugin) - if plugin.manual_commands and (not plugin.run_once_boolean or (plugin.run_once_boolean and (plugin.slug,) not in target.scans)): - with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: - if not heading: - file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) - heading = True - for description, commands in plugin.manual_commands.items(): - file.write('\t[-] ' + e(description) + '\n\n') - for command in commands: - file.write('\t\t' + e(command) + '\n\n') - file.flush() + for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'manual': + plugin.manual(service, plugin_was_run) + + if service.manual_commands and (not plugin.run_once_boolean or (plugin.run_once_boolean and (plugin.slug,) not in target.scans)): + with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: + if not heading: + file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) + heading = True + for description, commands in service.manual_commands.items(): + file.write('\t[-] ' + e(description) + '\n\n') + for command in commands: + file.write('\t\t' + e(command) + '\n\n') + file.flush() + + service.manual_commands = {} + break break @@ -1281,12 +1294,6 @@ async def main(): # Remove duplicate lists from list. [autorecon.excluded_tags.append(t) for t in excluded_tags if t not in autorecon.excluded_tags] - # Generate manual commands. - for _, plugin in autorecon.plugins.items(): - for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): - if member_name == 'manual': - plugin.manual() - raw_targets = args.targets if len(args.target_file) > 0: diff --git a/plugins/databases.py b/plugins/databases.py index e96d8a6..7b03051 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -23,8 +23,8 @@ class NmapMSSQL(ServiceScan): def configure(self): self.match_service_name(['^mssql', '^ms\-sql']) - def manual(self): - self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + def manual(self, service, plugin_was_run): + service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}') @@ -39,8 +39,8 @@ class NmapMYSQL(ServiceScan): def configure(self): self.match_service_name('^mysql') - def manual(self): - self.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + def manual(self, service, plugin_was_run): + service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}') @@ -55,8 +55,8 @@ class NmapOracle(ServiceScan): def configure(self): self.match_service_name('^oracle') - def manual(self): - self.add_manual_command('Brute-force SIDs using Nmap:', 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}') + def manual(self, service, plugin_was_run): + service.add_manual_command('Brute-force SIDs using Nmap:', 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}') async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}') @@ -98,8 +98,8 @@ class OracleODAT(ServiceScan): def configure(self): self.match_service_name('^oracle') - def manual(self): - self.add_manual_commands('Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:', [ + def manual(self, service, plugin_was_run): + service.add_manual_commands('Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:', [ 'python odat.py tnscmd -s {address} -p {port} --ping', 'python odat.py tnscmd -s {address} -p {port} --version', 'python odat.py tnscmd -s {address} -p {port} --status', @@ -118,5 +118,5 @@ class OraclePatator(ServiceScan): def configure(self): self.match_service_name('^oracle') - def manual(self): - self.add_manual_command('Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:', 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000') + def manual(self, service, plugin_was_run): + service.add_manual_command('Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:', 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000') diff --git a/plugins/ftp.py b/plugins/ftp.py index 456c44e..90a3b6f 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -23,8 +23,8 @@ class BruteforceFTP(ServiceScan): def configure(self): self.match_service_name(['^ftp', '^ftp\-data']) - def manual(self): - self.add_manual_commands('Bruteforce logins:', [ + def manual(self, service, plugin_was_run): + service.add_manual_commands('Bruteforce logins:', [ 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' ]) diff --git a/plugins/http.py b/plugins/http.py index 7a3321b..866cde3 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -29,8 +29,8 @@ class BruteforceHTTP(ServiceScan): self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ + def manual(self, service, plugin_was_run): + service.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{address}/path/to/auth/area', 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', @@ -85,28 +85,28 @@ class DirBuster(ServiceScan): self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + def manual(self, service, plugin_was_run): + service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ 'feroxbuster -u {http_scheme}://{address}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', 'feroxbuster -u {http_scheme}://{address}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' ]) - self.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + service.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) - self.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' ]) - self.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ + service.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ 'dirb {http_scheme}://{address}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_big.txt"', 'dirb {http_scheme}://{address}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' ]) - self.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + service.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) @@ -136,8 +136,8 @@ class Nikto(ServiceScan): self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') + def manual(self, service, plugin_was_run): + service.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') class WhatWeb(ServiceScan): @@ -182,5 +182,5 @@ class WPScan(ServiceScan): self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) - def manual(self): - self.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') + def manual(self, service, plugin_was_run): + service.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') diff --git a/plugins/ldap.py b/plugins/ldap.py index 2a08dca..3a6a53a 100644 --- a/plugins/ldap.py +++ b/plugins/ldap.py @@ -23,7 +23,7 @@ class LDAPSearch(ServiceScan): def configure(self): self.match_service_name('^ldap') - def manual(self): - self.add_manual_command('ldapsearch command (modify before running):', [ + def manual(self, service, plugin_was_run): + service.add_manual_command('ldapsearch command (modify before running):', [ 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' ]) diff --git a/plugins/misc.py b/plugins/misc.py index adf570e..ec847ec 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -131,8 +131,8 @@ class SMTPUserEnum(ServiceScan): await service.execute('hydra smtp-enum://{address}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') await service.execute('hydra smtp-enum://{address}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') - def manual(self): - self.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ + def manual(self, service, plugin_was_run): + service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ 'hydra smtp-enum://{address}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' ]) diff --git a/plugins/rdp.py b/plugins/rdp.py index f1dce27..d53a191 100644 --- a/plugins/rdp.py +++ b/plugins/rdp.py @@ -23,8 +23,8 @@ class BruteforceRDP(ServiceScan): def configure(self): self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) - def manual(self): - self.add_manual_commands('Bruteforce logins:', [ + def manual(self, service, plugin_was_run): + service.add_manual_commands('Bruteforce logins:', [ 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' ]) diff --git a/plugins/rpc.py b/plugins/rpc.py index be5beb9..e4d2419 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -23,5 +23,5 @@ class RPCClient(ServiceScan): def configure(self): self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) - def manual(self): - self.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') + def manual(self, service, plugin_was_run): + service.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') diff --git a/plugins/sip.py b/plugins/sip.py index 7553de0..a7ce898 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -23,5 +23,5 @@ class SIPVicious(ServiceScan): def configure(self): self.match_service_name('^asterisk') - def manual(self): - self.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') + def manual(self, service, plugin_was_run): + service.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') diff --git a/plugins/smb.py b/plugins/smb.py index e5bd73e..88da54e 100644 --- a/plugins/smb.py +++ b/plugins/smb.py @@ -10,8 +10,8 @@ class NmapSMB(ServiceScan): def configure(self): self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - def manual(self): - self.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ + def manual(self, service, plugin_was_run): + service.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' diff --git a/plugins/ssh.py b/plugins/ssh.py index d57d30e..bb75718 100644 --- a/plugins/ssh.py +++ b/plugins/ssh.py @@ -23,8 +23,8 @@ class BruteforceSSH(ServiceScan): def configure(self): self.match_service_name('ssh') - def manual(self): - self.add_manual_command('Bruteforce logins:', [ + def manual(self, service, plugin_was_run): + service.add_manual_command('Bruteforce logins:', [ 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' ]) From 666fdbcec5911af1cd00b2d4e5590f62b1ceb4de Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 15 Aug 2021 16:33:55 -0400 Subject: [PATCH 17/95] Update autorecon.py Fixed bug where inspect.signature() returned different values depending on python version. Using inspect.getfullargspec() instead. --- autorecon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autorecon.py b/autorecon.py index 0bec848..aea1534 100644 --- a/autorecon.py +++ b/autorecon.py @@ -470,11 +470,11 @@ class AutoRecon(object): if member_name == 'configure': configure_function_found = True elif member_name == 'run' and inspect.iscoroutinefunction(member_value): - if len(inspect.signature(member_value).parameters) != 2: + if len(inspect.getfullargspec(member_value).args) != 2: fail('Error: the "run" coroutine in the plugin "' + plugin.name + '" should have two arguments.', file=sys.stderr) run_coroutine_found = True elif member_name == 'manual': - if len(inspect.signature(member_value).parameters) != 3: + if len(inspect.getfullargspec(member_value).args) != 3: fail('Error: the "manual" function in the plugin "' + plugin.name + '" should have three arguments.', file=sys.stderr) manual_function_found = True From 9b1de4c940f012045d1c8ea5ee8446406333875d Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 15 Aug 2021 19:38:54 -0400 Subject: [PATCH 18/95] Update autorecon.py Changed add_service() method to make it more standard. --- autorecon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autorecon.py b/autorecon.py index aea1534..da26755 100644 --- a/autorecon.py +++ b/autorecon.py @@ -27,9 +27,9 @@ class Target: self.scans = [] self.running_tasks = {} - async def add_service(self, protocol, port, name, secure=False): + async def add_service(self, service): async with self.lock: - self.pending_services.append(Service(protocol, port, name, secure)) + self.pending_services.append(service) def extract_service(self, line, regex=None): return self.autorecon.extract_service(line, regex) From 11551135920195499cb3b45296c123dfe9423b6a Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 16 Aug 2021 22:46:15 -0400 Subject: [PATCH 19/95] Plugin Updates Better logic in wkhtmltoimage plugin. New Redis plugins. New RPCDump plugin. Updated README for new tools. --- README.md | 4 +++- plugins/http.py | 5 +++-- plugins/redis.py | 34 ++++++++++++++++++++++++++++++++++ plugins/rpc.py | 20 +++++++++++++++++++- 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 plugins/redis.py diff --git a/README.md b/README.md index e162a36..0112ba2 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,13 @@ Additionally the following commands may need to be installed, depending on your curl enum4linux feroxbuster +impacket-scripts nbtscan nikto nmap onesixtyone oscanner +redis-tools smbclient smbmap snmpwalk @@ -68,7 +70,7 @@ wkhtmltopdf On Kali Linux, you can ensure these are all installed using the following command: ```bash -$ sudo apt install seclists curl enum4linux feroxbuster nbtscan nikto nmap onesixtyone oscanner smbclient smbmap snmp sslscan sipvicious tnscmd10g whatweb wkhtmltopdf +$ sudo apt install seclists curl enum4linux feroxbuster impacket-scripts nbtscan nikto nmap onesixtyone oscanner redis-tools smbclient smbmap snmp sslscan sipvicious tnscmd10g whatweb wkhtmltopdf ``` ## Installation diff --git a/plugins/http.py b/plugins/http.py index 866cde3..719dbf8 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -166,8 +166,9 @@ class WkHTMLToImage(ServiceScan): self.match_service_name('^nacn_http$', negative_match=True) async def run(self, service): - if which('wkhtmltoimage') is not None and service.protocol == 'tcp': - await service.execute('wkhtmltoimage --format png {http_scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') + if which('wkhtmltoimage') is not None: + if service.protocol == 'tcp': + await service.execute('wkhtmltoimage --format png {http_scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') else: error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') diff --git a/plugins/redis.py b/plugins/redis.py new file mode 100644 index 0000000..8d816c2 --- /dev/null +++ b/plugins/redis.py @@ -0,0 +1,34 @@ +from autorecon import ServiceScan, error +from shutil import which + +class NmapRedis(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Redis' + self.tags = ['default', 'redis'] + + def configure(self): + self.match_service_name('^redis$') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,redis-info" -oN "{scandir}/{protocol}_{port}_redis_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_redis_nmap.xml" {address}') + +class RedisCli(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Redis Cli' + self.tags = ['default', 'redis'] + + def configure(self): + self.match_service_name('^redis$') + + async def run(self, service): + if which('redis-cli') is not None: + _, stdout, _ = await service.execute('redis-cli -p {port} -h {address} INFO', outfile='{protocol}_{port}_redis_info.txt') + if not (await stdout.readline()).startswith('NOAUTH Authentication required'): + await service.execute('redis-cli -p {port} -h {address} CONFIG GET \'*\'', outfile='{protocol}_{port}_redis_config.txt') + await service.execute('redis-cli -p {port} -h {address} CLIENT LIST', outfile='{protocol}_{port}_redis_client-list.txt') + else: + error('The redis-cli program could not be found. Make sure it is installed. (On Kali, run: sudo apt install redis-tools)') diff --git a/plugins/rpc.py b/plugins/rpc.py index e4d2419..17c0562 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -1,4 +1,5 @@ -from autorecon import ServiceScan +from autorecon import ServiceScan, error +from shutil import which class NmapMSRPC(ServiceScan): @@ -25,3 +26,20 @@ class RPCClient(ServiceScan): def manual(self, service, plugin_was_run): service.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') + +class RPCDump(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'rpcdump' + self.tags = ['default', 'rpc'] + + def configure(self): + self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) + + async def run(self, service): + if which('impacket-rpcdump') is not None: + if service.protocol == 'tcp': + await service.execute('impacket-rpcdump -port {port} {address}', outfile='{protocol}_{port}_rpc_rpcdump.txt') + else: + error('The impacket-rpcdump program could not be found. Make sure it is installed. (On Kali, run: sudo apt install impacket-scripts)') From 4d9145d326b4510ad6da58299533b276663b76b1 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 17 Aug 2021 19:03:29 -0400 Subject: [PATCH 20/95] Update autorecon.py Added --create-port-dirs option which will create port directories (e.g. tcp80, udp53) in the scans directory and put all relevant scans there. Now most command line options can be set in the config.toml file as well. --- autorecon.py | 123 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/autorecon.py b/autorecon.py index da26755..5ea7ef7 100644 --- a/autorecon.py +++ b/autorecon.py @@ -58,10 +58,10 @@ class Target: info('Port scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) if outfile is not None: - outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) + outfile = os.path.join(target.scandir, e(outfile)) if errfile is not None: - errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) + errfile = os.path.join(target.scandir, e(errfile)) async with target.lock: with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: @@ -121,6 +121,11 @@ class Service: port = self.port name = self.name + if target.autorecon.config['create_port_dirs']: + scandir = os.path.join(scandir, protocol + str(port)) + os.makedirs(scandir, exist_ok=True) + os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) + # Special cases for HTTP. http_scheme = 'https' if 'https' in self.name or self.secure is True else 'http' @@ -141,10 +146,10 @@ class Service: info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) if outfile is not None: - outfile = os.path.abspath(os.path.join(target.scandir, e(outfile))) + outfile = os.path.join(scandir, e(outfile)) if errfile is not None: - errfile = os.path.abspath(os.path.join(target.scandir, e(errfile))) + errfile = os.path.join(scandir, e(errfile)) async with target.lock: with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: @@ -376,18 +381,44 @@ class AutoRecon(object): self.tags = [] self.excluded_tags = [] self.patterns = [] - self.configurable_keys = ['max_scans', 'max_port_scans', 'single_target', 'outdir', 'only_scans_dir', 'heartbeat', 'timeout', 'target_timeout', 'accessible', 'verbose'] + self.configurable_keys = [ + 'max_scans', + 'max_port_scans', + 'tags', + 'exclude_tags', + 'plugins_dir', + 'outdir', + 'single_target', + 'only_scans_dir', + 'create_port_dirs', + 'heartbeat', + 'timeout', + 'target_timeout', + 'nmap', + 'nmap_append', + 'disable_sanity_checks', + 'accessible', + 'verbose' + ] + self.configurable_boolean_keys = ['single_target', 'only_scans_dir', 'create_port_dirs', 'disable_sanity_checks', 'accessible'] self.config = { 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], 'global_file': os.path.dirname(os.path.realpath(__file__)) + '/global.toml', 'max_scans': 50, 'max_port_scans': None, - 'single_target': False, + 'tags': 'default', + 'exclude_tags': None, + 'plugins_dir': os.path.dirname(os.path.abspath(__file__)) + '/plugins', 'outdir': 'results', + 'single_target': False, 'only_scans_dir': False, + 'create_port_dirs': False, 'heartbeat': 60, 'timeout': None, 'target_timeout': None, + 'nmap': '-vv --reason -Pn', + 'nmap_append': '', + 'disable_sanity_checks': False, 'accessible': False, 'verbose': 0 } @@ -806,27 +837,27 @@ async def scan_target(target): os.makedirs(basedir, exist_ok=True) if not target.autorecon.config['only_scans_dir']: - exploitdir = os.path.abspath(os.path.join(basedir, 'exploit')) + exploitdir = os.path.join(basedir, 'exploit') os.makedirs(exploitdir, exist_ok=True) - lootdir = os.path.abspath(os.path.join(basedir, 'loot')) + lootdir = os.path.join(basedir, 'loot') os.makedirs(lootdir, exist_ok=True) - reportdir = os.path.abspath(os.path.join(basedir, 'report')) + reportdir = os.path.join(basedir, 'report') target.reportdir = reportdir os.makedirs(reportdir, exist_ok=True) - open(os.path.abspath(os.path.join(reportdir, 'local.txt')), 'a').close() - open(os.path.abspath(os.path.join(reportdir, 'proof.txt')), 'a').close() + open(os.path.join(reportdir, 'local.txt'), 'a').close() + open(os.path.join(reportdir, 'proof.txt'), 'a').close() - screenshotdir = os.path.abspath(os.path.join(reportdir, 'screenshots')) + screenshotdir = os.path.join(reportdir, 'screenshots') os.makedirs(screenshotdir, exist_ok=True) - scandir = os.path.abspath(os.path.join(basedir, 'scans')) + scandir = os.path.join(basedir, 'scans') target.scandir = scandir os.makedirs(scandir, exist_ok=True) - os.makedirs(os.path.abspath(os.path.join(scandir, 'xml')), exist_ok=True) + os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) pending = [] @@ -1047,24 +1078,25 @@ async def main(): parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') - parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: 50') + parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: %(default)s') parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') - parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group.') - parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group.') - parser.add_argument('--plugins-dir', action='store', type=str, default=os.path.dirname(os.path.abspath(__file__)) + '/plugins', help='') - parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: results') - parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: false') - parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: false') - parser.add_argument('--heartbeat', action='store', type=int, help='Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60') - parser.add_argument('--timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: no timeout') - parser.add_argument('--target-timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: no timeout') + parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') + parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') + parser.add_argument('--plugins-dir', action='store', type=str, help='The location of the plugins directory. Default: %(default)s') + parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: %(default)s') + parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: %(default)s') + parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: %(default)s') + parser.add_argument('--create-port-dirs', action='store_true', help='Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and store results in these directories. Default: %(default)s') + parser.add_argument('--heartbeat', action='store', type=int, help='Specifies the heartbeat interval (in seconds) for scan status messages. Default: %(default)s') + parser.add_argument('--timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: %(default)s') + parser.add_argument('--target-timeout', action='store', type=int, help='Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: %(default)s') nmap_group = parser.add_mutually_exclusive_group() - nmap_group.add_argument('--nmap', action='store', default='-vv --reason -Pn', help='Override the {nmap_extra} variable in scans. Default: %(default)s') - nmap_group.add_argument('--nmap-append', action='store', default='', help='Append to the default {nmap_extra} variable in scans.') - parser.add_argument('--disable-sanity-checks', action='store_true', default=False, help='Disable sanity checks that would otherwise prevent the scans from running. Default: false') - parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders.') + nmap_group.add_argument('--nmap', action='store', help='Override the {nmap_extra} variable in scans. Default: %(default)s') + nmap_group.add_argument('--nmap-append', action='store', help='Append to the default {nmap_extra} variable in scans. Default: %(default)s') + parser.add_argument('--disable-sanity-checks', action='store_true', help='Disable sanity checks that would otherwise prevent the scans from running. Default: %(default)s') + parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders. Default: %(default)s') parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') parser.add_argument('--version', action='store_true', help='Prints the AutoRecon version and exits.') parser.error = lambda s: fail(s[0].upper() + s[1:]) @@ -1086,9 +1118,10 @@ async def main(): try: config_toml = toml.load(c) for key, val in config_toml.items(): - if key.replace('-', '_') == 'global_file': + if key == 'global-file': autorecon.config['global_file'] = val - break + elif key == 'plugins-dir': + autorecon.config['plugins_dir'] = val except toml.decoder.TomlDecodeError: fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.') @@ -1096,19 +1129,20 @@ async def main(): for key in args_dict: if key == 'global_file' and args_dict['global_file'] is not None: autorecon.config['global_file'] = args_dict['global_file'] - break + elif key == 'plugins_dir' and args_dict['plugins_dir'] is not None: + autorecon.config['plugins_dir'] = args_dict['plugins_dir'] - if not os.path.isdir(args.plugins_dir): - fail('Error: Specified plugins directory "' + args.plugins_dir + '" does not exist.') + if not os.path.isdir(autorecon.config['plugins_dir']): + fail('Error: Specified plugins directory "' + autorecon.config['plugins_dir'] + '" does not exist.') - for plugin_file in os.listdir(args.plugins_dir): + for plugin_file in os.listdir(autorecon.config['plugins_dir']): if not plugin_file.startswith('_') and plugin_file.endswith('.py'): dirname, filename = os.path.split(plugin_file) dirname = os.path.abspath(dirname) try: - plugin = importlib.import_module('.' + filename[:-3], os.path.basename(args.plugins_dir)) + plugin = importlib.import_module('.' + filename[:-3], os.path.basename(autorecon.config['plugins_dir'])) clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) for (_, c) in clsmembers: if c.__module__ == 'autorecon': @@ -1129,7 +1163,7 @@ async def main(): sys.exit(1) if len(autorecon.plugin_types['port']) == 0: - fail('Error: There are no valid PortScan plugins in the plugins directory "' + args.plugins_dir + '".') + fail('Error: There are no valid PortScan plugins in the plugins directory "' + autorecon.config['plugins_dir'] + '".') # Sort plugins by priority. autorecon.plugin_types['port'].sort(key=lambda x: x.priority) @@ -1192,6 +1226,7 @@ async def main(): except toml.decoder.TomlDecodeError: fail('Error: Couldn\'t parse ' + g.name + ' file. Check syntax.') + other_options = [] for key, val in config_toml.items(): if key == 'global' and isinstance(val, dict): # Process global plugin options. for gkey, gval in config_toml['global'].items(): @@ -1223,9 +1258,15 @@ async def main(): if autorecon.argparse.get_default(slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_')): autorecon.argparse.set_defaults(**{slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): pval}) else: # Process potential other options. - if key.replace('-', '_') in autorecon.configurable_keys: - autorecon.config[key.replace('-', '_')] = val - autorecon.argparse.set_defaults(**{key.replace('-', '_'): val}) + key = key.replace('-', '_') + if key in autorecon.configurable_keys: + other_options.append(key) + autorecon.config[key] = val + autorecon.argparse.set_defaults(**{key: val}) + + for key, val in autorecon.config.items(): + if key not in other_options: + autorecon.argparse.set_defaults(**{key: val}) parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='Show this help message and exit.') args = parser.parse_args() @@ -1234,7 +1275,7 @@ async def main(): for key in args_dict: if key in autorecon.configurable_keys and args_dict[key] is not None: # Special case for booleans - if key in ['accessible', 'single_target', 'only_scans_dir'] and autorecon.config[key]: + if key in autorecon.configurable_boolean_keys and autorecon.config[key]: continue autorecon.config[key] = args_dict[key] @@ -1287,6 +1328,8 @@ async def main(): [autorecon.tags.append(t) for t in tags if t not in autorecon.tags] excluded_tags = [] + if args.exclude_tags is None: + args.exclude_tags = '' if args.exclude_tags != '': for tag_group in list(set(filter(None, args.exclude_tags.lower().split(',')))): excluded_tags.append(list(set(filter(None, tag_group.split('+'))))) From b94ac0065de29929ab2e366c06157d64abba4ef1 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 18 Aug 2021 15:36:03 -0400 Subject: [PATCH 21/95] Update http.py Re-arranged dirbuster run/manual functions. --- plugins/http.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/plugins/http.py b/plugins/http.py index 719dbf8..62445b3 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -85,6 +85,20 @@ class DirBuster(ServiceScan): self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) + async def run(self, service): + for wordlist in self.get_option('wordlist'): + name = os.path.splitext(os.path.basename(wordlist))[0] + if self.get_option('tool') == 'feroxbuster': + await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "txt,html,php,asp,aspx,jsp" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') + elif self.get_option('tool') == 'gobuster': + await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') + elif self.get_option('tool') == 'dirsearch': + await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w ' + wordlist + ' --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') + elif self.get_option('tool') == 'ffuf': + await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e ".txt,.html,.php,.asp,.aspx,.jsp" -v | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') + elif self.get_option('tool') == 'dirb': + await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') + def manual(self, service, plugin_was_run): service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ 'feroxbuster -u {http_scheme}://{address}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', @@ -111,20 +125,6 @@ class DirBuster(ServiceScan): 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) - async def run(self, service): - for wordlist in self.get_option('wordlist'): - name = os.path.splitext(os.path.basename(wordlist))[0] - if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "txt,html,php,asp,aspx,jsp" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') - elif self.get_option('tool') == 'gobuster': - await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') - elif self.get_option('tool') == 'dirsearch': - await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w ' + wordlist + ' --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') - elif self.get_option('tool') == 'ffuf': - await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e ".txt,.html,.php,.asp,.aspx,.jsp" -v | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') - elif self.get_option('tool') == 'dirb': - await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') - class Nikto(ServiceScan): def __init__(self): From 12f53a39623c1a5b3b3095baa6a2774fe03859e1 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 18 Aug 2021 15:36:16 -0400 Subject: [PATCH 22/95] Update dns.py Added Zone Transfer plugin for DNS. --- plugins/dns.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/dns.py b/plugins/dns.py index a84324a..46c2b99 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -12,3 +12,20 @@ class DNS(ServiceScan): async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}') + +class ZoneTransfer(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Zone Transfer" + self.tags = ['default', 'dns'] + + def configure(self): + self.match_service_name('^domain') + self.add_option('domain', help='The domain name to perform a zone transfer on.') + + async def run(self, service): + if self.get_option('domain') is None: + await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') + else: + await service.execute('dig AXFR ' + self.get_option('domain') + ' -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') From 429434f1a84cfd13e7802165531740dafbea6557 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 18 Aug 2021 18:44:26 -0400 Subject: [PATCH 23/95] Added DNS Reverse Lookup plugin Also renamed Zone Transfer plugin to DNS Zone Transfer. --- plugins/dns.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/dns.py b/plugins/dns.py index 46c2b99..45d3b3b 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -13,11 +13,11 @@ class DNS(ServiceScan): async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}') -class ZoneTransfer(ServiceScan): +class DNSZoneTransfer(ServiceScan): def __init__(self): super().__init__() - self.name = "Zone Transfer" + self.name = "DNS Zone Transfer" self.tags = ['default', 'dns'] def configure(self): @@ -28,4 +28,17 @@ class ZoneTransfer(ServiceScan): if self.get_option('domain') is None: await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') else: - await service.execute('dig AXFR ' + self.get_option('domain') + ' -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') + await service.execute('dig AXFR -p {port} @{address} ' + self.get_option('domain'), outfile='{protocol}_{port}_dns_zone-transfer.txt') + +class DNSReverseLookup(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "DNS Reverse Lookup" + self.tags = ['default', 'dns'] + + def configure(self): + self.match_service_name('^domain') + + async def run(self, service): + await service.execute('dig -p {port} -x {address} @{address}', outfile='{protocol}_{port}_dns_reverse-lookup.txt') From 8ed8c748823e5a92bd54e3221929fc04e6723a0c Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Thu, 19 Aug 2021 23:18:43 -0400 Subject: [PATCH 24/95] Bug fixes. Fixed bug where commands were double formatted when being written to _commands.txt Fixed bug where plugin warnings would print multiple times per plugin. --- autorecon.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/autorecon.py b/autorecon.py index 5ea7ef7..99c4288 100644 --- a/autorecon.py +++ b/autorecon.py @@ -153,7 +153,7 @@ class Service: async with target.lock: with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: - file.writelines(e('{cmd}\n\n')) + file.writelines(cmd + '\n\n') process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) @@ -982,29 +982,29 @@ async def scan_target(target): # Skip plugin if run_once_boolean and plugin already in target scans if plugin.run_once_boolean and (plugin.slug,) in target.scans: warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') - continue + break # Skip plugin if require_ssl_boolean and port is not secure if plugin.require_ssl_boolean and not service.secure: plugin_service_match = False - continue + break # Skip plugin if service port is in ignore_ports: if port in plugin.ignore_ports[protocol]: plugin_service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') - continue + break # Skip plugin if plugin has required ports and service port is not in them: if plugin.ports[protocol] and port not in plugin.ports[protocol]: plugin_service_match = False warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}') - continue + break for i in plugin.ignore_service_names: if re.search(i, service.name): warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against this service. Skipping.{rst}') - continue + break # TODO: check if plugin matches tags, BUT run manual commands anyway! plugin_was_run = True From 005a729dd9aeed160bb9dbaf0081d8cffc4d7a9c Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 20 Aug 2021 13:22:55 -0400 Subject: [PATCH 25/95] Plugin Updates Added global option "domain" for use with DNS / Active Directory. Updated DNS and Kerberos plugins. --- global.toml | 4 ++++ plugins/dns.py | 7 +++---- plugins/kerberos.py | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/global.toml b/global.toml index 47c7edc..2ea6ce1 100644 --- a/global.toml +++ b/global.toml @@ -5,3 +5,7 @@ help = 'A wordlist of usernames, useful for bruteforcing. Default: %(default)s' [global.password-wordlist] default = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' help = 'A wordlist of passwords, useful for bruteforcing. Default: %(default)s' + +[global.domain] +default = false +help = 'The domain to use (if known). Used for DNS and/or Active Directory.' diff --git a/plugins/dns.py b/plugins/dns.py index 45d3b3b..249b846 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -22,13 +22,12 @@ class DNSZoneTransfer(ServiceScan): def configure(self): self.match_service_name('^domain') - self.add_option('domain', help='The domain name to perform a zone transfer on.') async def run(self, service): - if self.get_option('domain') is None: - await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') + if self.get_global('domain'): + await service.execute('dig AXFR -p {port} @{address} ' + self.get_global('domain'), outfile='{protocol}_{port}_dns_zone-transfer.txt') else: - await service.execute('dig AXFR -p {port} @{address} ' + self.get_option('domain'), outfile='{protocol}_{port}_dns_zone-transfer.txt') + await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') class DNSReverseLookup(ServiceScan): diff --git a/plugins/kerberos.py b/plugins/kerberos.py index 5637f56..d13f1db 100644 --- a/plugins/kerberos.py +++ b/plugins/kerberos.py @@ -11,4 +11,7 @@ class NmapKerberos(ServiceScan): self.match_service_name(['^kerberos', '^kpasswd']) async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') + if self.get_global('domain') and self.get_global('username-wordlist'): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" --script-args krb5-enum-users.realm="' + self.get_global('domain') + '",userdb="' + self.get_global('username-wordlist') + '" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') + else: + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') From 4d77ec2ed7e80fa95416c93be925c793cb0b40a5 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 21 Aug 2021 22:20:54 -0400 Subject: [PATCH 26/95] Create rsync.py Added rsync plugins. --- plugins/rsync.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 plugins/rsync.py diff --git a/plugins/rsync.py b/plugins/rsync.py new file mode 100644 index 0000000..84e3b53 --- /dev/null +++ b/plugins/rsync.py @@ -0,0 +1,27 @@ +from autorecon import ServiceScan + +class NmapRsync(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Rsync' + self.tags = ['default', 'rsync'] + + def configure(self): + self.match_service_name('^rsync') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rsync* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rsync_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rsync_nmap.xml" {address}') + +class RsyncList(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Rsync List Files' + self.tags = ['default', 'rsync'] + + def configure(self): + self.match_service_name('^rsync') + + async def run(self, service): + await service.execute('rsync -av --list-only rsync://{address}:{port}', outfile='{protocol}_{port}_rsync_file_list.txt') From 766dd870e5b0b02a0675bf1d71d534444d6e1bb5 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 22 Aug 2021 08:04:34 -0400 Subject: [PATCH 27/95] Update autorecon.py Added filename reference to plugin registering errors. --- autorecon.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/autorecon.py b/autorecon.py index 99c4288..8ddac41 100644 --- a/autorecon.py +++ b/autorecon.py @@ -468,27 +468,27 @@ class AutoRecon(object): break return services - def register(self, plugin): + def register(self, plugin, filename): if plugin.disabled: return for _, loaded_plugin in self.plugins.items(): if plugin.name == loaded_plugin.name: - fail('Error: Duplicate plugin name "' + plugin.name + '" detected.', file=sys.stderr) + fail('Error: Duplicate plugin name "' + plugin.name + '" detected in ' + filename + '.', file=sys.stderr) if plugin.slug is None: plugin.slug = slugify(plugin.name) elif not self.__slug_regex.match(plugin.slug): - fail('Error: provided slug "' + plugin.slug + '" is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) + fail('Error: provided slug "' + plugin.slug + '" in ' + filename + ' is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) if plugin.slug in self.config['protected_classes']: - fail('Error: plugin slug "' + plugin.slug + '" is a protected string. Please change.') + fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is a protected string. Please change.') if plugin.slug not in self.plugins: for _, loaded_plugin in self.plugins.items(): if plugin is loaded_plugin: - fail('Error: plugin "' + plugin.name + '" already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) + fail('Error: plugin "' + plugin.name + '" in ' + filename + ' already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) if plugin.description is None: plugin.description = '' @@ -502,15 +502,15 @@ class AutoRecon(object): configure_function_found = True elif member_name == 'run' and inspect.iscoroutinefunction(member_value): if len(inspect.getfullargspec(member_value).args) != 2: - fail('Error: the "run" coroutine in the plugin "' + plugin.name + '" should have two arguments.', file=sys.stderr) + fail('Error: the "run" coroutine in the plugin "' + plugin.name + '" in ' + filename + ' should have two arguments.', file=sys.stderr) run_coroutine_found = True elif member_name == 'manual': if len(inspect.getfullargspec(member_value).args) != 3: - fail('Error: the "manual" function in the plugin "' + plugin.name + '" should have three arguments.', file=sys.stderr) + fail('Error: the "manual" function in the plugin "' + plugin.name + '" in ' + filename + ' should have three arguments.', file=sys.stderr) manual_function_found = True if not run_coroutine_found and not manual_function_found: - fail('Error: the plugin "' + plugin.name + '" needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) + fail('Error: the plugin "' + plugin.name + '" in ' + filename + ' needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) from autorecon import PortScan, ServiceScan if issubclass(plugin.__class__, PortScan): @@ -518,7 +518,7 @@ class AutoRecon(object): elif issubclass(plugin.__class__, ServiceScan): self.plugin_types["service"].append(plugin) else: - fail('Plugin "' + plugin.name + '" is neither a PortScan nor a ServiceScan.', file=sys.stderr) + fail('Plugin "' + plugin.name + '" in ' + filename + ' is neither a PortScan nor a ServiceScan.', file=sys.stderr) plugin.tags = [tag.lower() for tag in plugin.tags] @@ -527,7 +527,7 @@ class AutoRecon(object): plugin.configure() self.plugins[plugin.slug] = plugin else: - fail('Error: plugin slug "' + plugin.slug + '" is already assigned.', file=sys.stderr) + fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is already assigned.', file=sys.stderr) async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None): if patterns: @@ -1154,7 +1154,7 @@ async def main(): # Only add classes that are a sub class of either PortScan or ServiceScan if issubclass(c, PortScan) or issubclass(c, ServiceScan): - autorecon.register(c()) + autorecon.register(c(), filename) else: print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.') except (ImportError, SyntaxError) as ex: @@ -1475,6 +1475,7 @@ async def main(): elapsed_time = calculate_elapsed_time(start_time) info('{bright}Finished scanning all targets in ' + elapsed_time + '!{rst}') + info('{bright}Don\'t forget to check out more commands to run manually in the _manual_commands.txt file in each target\'s scans directory!') if autorecon.missing_services: warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins based on the service name. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') From 741a0bf9d07d7841c27751f7b58a415457a35374 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 22 Aug 2021 08:10:23 -0400 Subject: [PATCH 28/95] Plugin updates. Added "safe" tag to most plugins. Added an "unsafe" non-default tag. Created an SMB Vulnerabilities plugin which is unsafe and won't run by default. --- plugins/databases.py | 14 +++++------ plugins/dns.py | 6 ++--- plugins/ftp.py | 2 +- plugins/http.py | 16 ++++++------- plugins/kerberos.py | 2 +- plugins/ldap.py | 4 ++-- plugins/misc.py | 55 +++++++++----------------------------------- plugins/nfs.py | 4 ++-- plugins/rdp.py | 2 +- plugins/redis.py | 4 ++-- plugins/rpc.py | 6 ++--- plugins/rsync.py | 4 ++-- plugins/sip.py | 4 ++-- plugins/smb.py | 40 ++++++++++++++++++++++---------- plugins/smtp.py | 33 ++++++++++++++++++++++++++ plugins/snmp.py | 6 ++--- plugins/sslscan.py | 2 +- 17 files changed, 110 insertions(+), 94 deletions(-) create mode 100644 plugins/smtp.py diff --git a/plugins/databases.py b/plugins/databases.py index 7b03051..b45d320 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -5,7 +5,7 @@ class NmapMongoDB(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap MongoDB" - self.tags = ['default', 'databases'] + self.tags = ['default', 'safe', 'databases'] def configure(self): self.match_service_name('^mongod') @@ -18,7 +18,7 @@ class NmapMSSQL(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap MSSQL" - self.tags = ['default', 'databases'] + self.tags = ['default', 'safe', 'databases'] def configure(self): self.match_service_name(['^mssql', '^ms\-sql']) @@ -34,7 +34,7 @@ class NmapMYSQL(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap MYSQL" - self.tags = ['default', 'databases'] + self.tags = ['default', 'safe', 'databases'] def configure(self): self.match_service_name('^mysql') @@ -50,7 +50,7 @@ class NmapOracle(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap Oracle" - self.tags = ['default', 'databases'] + self.tags = ['default', 'safe', 'databases'] def configure(self): self.match_service_name('^oracle') @@ -66,7 +66,7 @@ class OracleTNScmd(ServiceScan): def __init__(self): super().__init__() self.name = "Oracle TNScmd" - self.tags = ['default', 'databases'] + self.tags = ['default', 'safe', 'databases'] def configure(self): self.match_service_name('^oracle') @@ -80,7 +80,7 @@ class OracleScanner(ServiceScan): def __init__(self): super().__init__() self.name = "Oracle Scanner" - self.tags = ['default', 'databases'] + self.tags = ['default', 'safe', 'databases'] def configure(self): self.match_service_name('^oracle') @@ -93,7 +93,7 @@ class OracleODAT(ServiceScan): def __init__(self): super().__init__() self.name = "Oracle ODAT" - self.tags = ['default', 'databases'] + self.tags = ['default', 'safe', 'databases'] def configure(self): self.match_service_name('^oracle') diff --git a/plugins/dns.py b/plugins/dns.py index 249b846..50fddfd 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -5,7 +5,7 @@ class DNS(ServiceScan): def __init__(self): super().__init__() self.name = "DNS" - self.tags = ['default', 'dns'] + self.tags = ['default', 'safe', 'dns'] def configure(self): self.match_service_name('^domain') @@ -18,7 +18,7 @@ class DNSZoneTransfer(ServiceScan): def __init__(self): super().__init__() self.name = "DNS Zone Transfer" - self.tags = ['default', 'dns'] + self.tags = ['default', 'safe', 'dns'] def configure(self): self.match_service_name('^domain') @@ -34,7 +34,7 @@ class DNSReverseLookup(ServiceScan): def __init__(self): super().__init__() self.name = "DNS Reverse Lookup" - self.tags = ['default', 'dns'] + self.tags = ['default', 'safe', 'dns'] def configure(self): self.match_service_name('^domain') diff --git a/plugins/ftp.py b/plugins/ftp.py index 90a3b6f..386a0db 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -5,7 +5,7 @@ class NmapFTP(ServiceScan): def __init__(self): super().__init__() self.name = 'Nmap FTP' - self.tags = ['default', 'ftp'] + self.tags = ['default', 'safe', 'ftp'] def configure(self): self.match_service_name(['^ftp', '^ftp\-data']) diff --git a/plugins/http.py b/plugins/http.py index 62445b3..052e1be 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -7,7 +7,7 @@ class NmapHTTP(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap HTTP" - self.tags = ['default', 'http'] + self.tags = ['default', 'safe', 'http'] def configure(self): self.match_service_name('^http') @@ -42,7 +42,7 @@ class Curl(ServiceScan): def __init__(self): super().__init__() self.name = "Curl" - self.tags = ['default', 'http'] + self.tags = ['default', 'safe', 'http'] def configure(self): self.add_option("path", default="/", help="The path on the web server to curl. Default: %(default)s") @@ -59,7 +59,7 @@ class CurlRobots(ServiceScan): def __init__(self): super().__init__() self.name = "Curl Robots" - self.tags = ['default', 'http'] + self.tags = ['default', 'safe', 'http'] def configure(self): self.match_service_name('^http') @@ -76,7 +76,7 @@ class DirBuster(ServiceScan): self.name = "DirBuster" self.slug = 'dirbuster' self.priority = 0 - self.tags = ['default', 'http', 'long'] + self.tags = ['default', 'safe', 'long', 'http'] def configure(self): self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') @@ -130,7 +130,7 @@ class Nikto(ServiceScan): def __init__(self): super().__init__() self.name = 'nikto' - self.tags = ['default', 'http', 'long'] + self.tags = ['default', 'safe', 'long', 'http'] def configure(self): self.match_service_name('^http') @@ -144,7 +144,7 @@ class WhatWeb(ServiceScan): def __init__(self): super().__init__() self.name = "whatweb" - self.tags = ['default', 'http'] + self.tags = ['default', 'safe', 'http'] def configure(self): self.match_service_name('^http') @@ -159,7 +159,7 @@ class WkHTMLToImage(ServiceScan): def __init__(self): super().__init__() self.name = "wkhtmltoimage" - self.tags = ['default', 'http'] + self.tags = ['default', 'safe', 'http'] def configure(self): self.match_service_name('^http') @@ -177,7 +177,7 @@ class WPScan(ServiceScan): def __init__(self): super().__init__() self.name = 'WPScan' - self.tags = ['default', 'http'] + self.tags = ['default', 'safe', 'http'] def configure(self): self.match_service_name('^http') diff --git a/plugins/kerberos.py b/plugins/kerberos.py index d13f1db..488c615 100644 --- a/plugins/kerberos.py +++ b/plugins/kerberos.py @@ -5,7 +5,7 @@ class NmapKerberos(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap Kerberos" - self.tags = ['default', 'kerberos', 'active-directory'] + self.tags = ['default', 'safe', 'kerberos', 'active-directory'] def configure(self): self.match_service_name(['^kerberos', '^kpasswd']) diff --git a/plugins/ldap.py b/plugins/ldap.py index 3a6a53a..b82f322 100644 --- a/plugins/ldap.py +++ b/plugins/ldap.py @@ -5,7 +5,7 @@ class NmapLDAP(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap LDAP" - self.tags = ['default', 'ldap', 'active-directory'] + self.tags = ['default', 'safe', 'ldap', 'active-directory'] def configure(self): self.match_service_name('^ldap') @@ -18,7 +18,7 @@ class LDAPSearch(ServiceScan): def __init__(self): super().__init__() self.name = 'LDAP Search' - self.tags = ['default', 'ldap', 'active-directory'] + self.tags = ['default', 'safe', 'ldap', 'active-directory'] def configure(self): self.match_service_name('^ldap') diff --git a/plugins/misc.py b/plugins/misc.py index ec847ec..5af0105 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -5,7 +5,7 @@ class NmapCassandra(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap Cassandra" - self.tags = ['default', 'cassandra'] + self.tags = ['default', 'safe', 'cassandra'] def configure(self): self.match_service_name('^apani1') @@ -18,7 +18,7 @@ class NmapCUPS(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap CUPS" - self.tags = ['default', 'cups'] + self.tags = ['default', 'safe', 'cups'] def configure(self): self.match_service_name('^ipp') @@ -31,7 +31,7 @@ class NmapDistccd(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap distccd" - self.tags = ['default', 'distccd'] + self.tags = ['default', 'safe', 'distccd'] def configure(self): self.match_service_name('^distccd') @@ -44,7 +44,7 @@ class NmapFinger(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap finger" - self.tags = ['default', 'finger'] + self.tags = ['default', 'safe', 'finger'] def configure(self): self.match_service_name('^finger') @@ -57,7 +57,7 @@ class NmapIMAP(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap IMAP" - self.tags = ['default', 'imap', 'email'] + self.tags = ['default', 'safe', 'imap', 'email'] def configure(self): self.match_service_name('^imap') @@ -70,7 +70,7 @@ class NmapNNTP(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap NNTP" - self.tags = ['default', 'nntp'] + self.tags = ['default', 'safe', 'nntp'] def configure(self): self.match_service_name('^nntp') @@ -83,7 +83,7 @@ class NmapPOP3(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap POP3" - self.tags = ['default', 'pop3', 'email'] + self.tags = ['default', 'safe', 'pop3', 'email'] def configure(self): self.match_service_name('^pop3') @@ -96,7 +96,7 @@ class NmapRMI(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap RMI" - self.tags = ['default', 'rmi'] + self.tags = ['default', 'safe', 'rmi'] def configure(self): self.match_service_name(['^java\-rmi', '^rmiregistry']) @@ -104,45 +104,12 @@ class NmapRMI(ServiceScan): async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}') -class NmapSMTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap SMTP" - self.tags = ['default', 'smtp', 'email'] - - def configure(self): - self.match_service_name('^smtp') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}') - -class SMTPUserEnum(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'SMTP-User-Enum' - self.tags = ['default', 'smtp', 'email'] - - def configure(self): - self.match_service_name('^smtp') - - async def run(self, service): - await service.execute('hydra smtp-enum://{address}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') - await service.execute('hydra smtp-enum://{address}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') - - def manual(self, service, plugin_was_run): - service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ - 'hydra smtp-enum://{address}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' - ]) - - class NmapTelnet(ServiceScan): def __init__(self): super().__init__() self.name = 'Nmap Telnet' - self.tags = ['default', 'telnet'] + self.tags = ['default', 'safe', 'telnet'] def configure(self): self.match_service_name('^telnet') @@ -155,7 +122,7 @@ class NmapTFTP(ServiceScan): def __init__(self): super().__init__() self.name = 'Nmap TFTP' - self.tags = ['default', 'tftp'] + self.tags = ['default', 'safe', 'tftp'] def configure(self): self.match_service_name('^tftp') @@ -168,7 +135,7 @@ class NmapVNC(ServiceScan): def __init__(self): super().__init__() self.name = 'Nmap VNC' - self.tags = ['default', 'vnc'] + self.tags = ['default', 'safe', 'vnc'] def configure(self): self.match_service_name('^vnc') diff --git a/plugins/nfs.py b/plugins/nfs.py index d47df84..b1a96e3 100644 --- a/plugins/nfs.py +++ b/plugins/nfs.py @@ -5,7 +5,7 @@ class NmapNFS(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap NFS" - self.tags = ['default', 'nfs'] + self.tags = ['default', 'safe', 'nfs'] def configure(self): self.match_service_name(['^nfs', '^rpcbind']) @@ -18,7 +18,7 @@ class Showmount(ServiceScan): def __init__(self): super().__init__() self.name = "showmount" - self.tags = ['default', 'nfs'] + self.tags = ['default', 'safe', 'nfs'] def configure(self): self.match_service_name(['^nfs', '^rpcbind']) diff --git a/plugins/rdp.py b/plugins/rdp.py index d53a191..f075e71 100644 --- a/plugins/rdp.py +++ b/plugins/rdp.py @@ -5,7 +5,7 @@ class NmapRDP(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap RDP" - self.tags = ['default', 'rdp'] + self.tags = ['default', 'safe', 'rdp'] def configure(self): self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) diff --git a/plugins/redis.py b/plugins/redis.py index 8d816c2..933d0f7 100644 --- a/plugins/redis.py +++ b/plugins/redis.py @@ -6,7 +6,7 @@ class NmapRedis(ServiceScan): def __init__(self): super().__init__() self.name = 'Nmap Redis' - self.tags = ['default', 'redis'] + self.tags = ['default', 'safe', 'redis'] def configure(self): self.match_service_name('^redis$') @@ -19,7 +19,7 @@ class RedisCli(ServiceScan): def __init__(self): super().__init__() self.name = 'Redis Cli' - self.tags = ['default', 'redis'] + self.tags = ['default', 'safe', 'redis'] def configure(self): self.match_service_name('^redis$') diff --git a/plugins/rpc.py b/plugins/rpc.py index 17c0562..79ef7cf 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -1,7 +1,7 @@ from autorecon import ServiceScan, error from shutil import which -class NmapMSRPC(ServiceScan): +class NmapRPC(ServiceScan): def __init__(self): super().__init__() @@ -19,7 +19,7 @@ class RPCClient(ServiceScan): def __init__(self): super().__init__() self.name = "rpcclient" - self.tags = ['default', 'rpc'] + self.tags = ['default', 'safe', 'rpc'] def configure(self): self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) @@ -32,7 +32,7 @@ class RPCDump(ServiceScan): def __init__(self): super().__init__() self.name = 'rpcdump' - self.tags = ['default', 'rpc'] + self.tags = ['default', 'safe', 'rpc'] def configure(self): self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) diff --git a/plugins/rsync.py b/plugins/rsync.py index 84e3b53..52c02d5 100644 --- a/plugins/rsync.py +++ b/plugins/rsync.py @@ -5,7 +5,7 @@ class NmapRsync(ServiceScan): def __init__(self): super().__init__() self.name = 'Nmap Rsync' - self.tags = ['default', 'rsync'] + self.tags = ['default', 'safe', 'rsync'] def configure(self): self.match_service_name('^rsync') @@ -18,7 +18,7 @@ class RsyncList(ServiceScan): def __init__(self): super().__init__() self.name = 'Rsync List Files' - self.tags = ['default', 'rsync'] + self.tags = ['default', 'safe', 'rsync'] def configure(self): self.match_service_name('^rsync') diff --git a/plugins/sip.py b/plugins/sip.py index a7ce898..bbbe24c 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -5,7 +5,7 @@ class NmapSIP(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap SIP" - self.tags = ['default', 'sip'] + self.tags = ['default', 'safe', 'sip'] def configure(self): self.match_service_name('^asterisk') @@ -18,7 +18,7 @@ class SIPVicious(ServiceScan): def __init__(self): super().__init__() self.name = "SIPVicious" - self.tags = ['default', 'sip'] + self.tags = ['default', 'safe', 'sip'] def configure(self): self.match_service_name('^asterisk') diff --git a/plugins/smb.py b/plugins/smb.py index 88da54e..70ea2ff 100644 --- a/plugins/smb.py +++ b/plugins/smb.py @@ -5,27 +5,43 @@ class NmapSMB(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap SMB" - self.tags = ['default', 'smb', 'active-directory'] + self.tags = ['default', 'safe', 'smb', 'active-directory'] def configure(self): self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - def manual(self, service, plugin_was_run): - service.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' - ]) - async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}') +class SMBVuln(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SMB Vulnerabilities" + self.tags = ['unsafe', 'smb', 'active-directory'] + + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}') + await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}') + await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}') + + def manual(self, service, plugin_was_run): + if not plugin_was_run: # Only suggest these if they weren't run. + service.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' + ]) + class Enum4Linux(ServiceScan): def __init__(self): super().__init__() self.name = "Enum4Linux" - self.tags = ['default', 'enum4linux', 'active-directory'] + self.tags = ['default', 'safe', 'enum4linux', 'active-directory'] def configure(self): self.match_service_name(['^ldap', '^smb', '^microsoft\-ds', '^netbios']) @@ -41,7 +57,7 @@ class NBTScan(ServiceScan): def __init__(self): super().__init__() self.name = "nbtscan" - self.tags = ['default', 'netbios', 'active-directory'] + self.tags = ['default', 'safe', 'netbios', 'active-directory'] def configure(self): self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) @@ -56,7 +72,7 @@ class SMBClient(ServiceScan): def __init__(self): super().__init__() self.name = "SMBClient" - self.tags = ['default', 'smb', 'active-directory'] + self.tags = ['default', 'safe', 'smb', 'active-directory'] def configure(self): self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) @@ -71,7 +87,7 @@ class SMBMap(ServiceScan): def __init__(self): super().__init__() self.name = "SMBMap" - self.tags = ['default', 'smb', 'active-directory'] + self.tags = ['default', 'safe', 'smb', 'active-directory'] def configure(self): self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) diff --git a/plugins/smtp.py b/plugins/smtp.py new file mode 100644 index 0000000..50ec9c5 --- /dev/null +++ b/plugins/smtp.py @@ -0,0 +1,33 @@ +from autorecon import ServiceScan + +class NmapSMTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SMTP" + self.tags = ['default', 'safe', 'smtp', 'email'] + + def configure(self): + self.match_service_name('^smtp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}') + +class SMTPUserEnum(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'SMTP-User-Enum' + self.tags = ['default', 'safe', 'smtp', 'email'] + + def configure(self): + self.match_service_name('^smtp') + + async def run(self, service): + await service.execute('hydra smtp-enum://{address}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') + await service.execute('hydra smtp-enum://{address}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') + + def manual(self, service, plugin_was_run): + service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ + 'hydra smtp-enum://{address}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' + ]) diff --git a/plugins/snmp.py b/plugins/snmp.py index 05b63ce..2a4ea25 100644 --- a/plugins/snmp.py +++ b/plugins/snmp.py @@ -5,7 +5,7 @@ class NmapSNMP(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap SNMP" - self.tags = ['default', 'snmp'] + self.tags = ['default', 'safe', 'snmp'] def configure(self): self.match_service_name('^snmp') @@ -18,7 +18,7 @@ class OneSixtyOne(ServiceScan): def __init__(self): super().__init__() self.name = "OneSixtyOne" - self.tags = ['default', 'snmp'] + self.tags = ['default', 'safe', 'snmp'] def configure(self): self.match_service_name('^snmp') @@ -34,7 +34,7 @@ class SNMPWalk(ServiceScan): def __init__(self): super().__init__() self.name = "SNMPWalk" - self.tags = ['default', 'snmp'] + self.tags = ['default', 'safe', 'snmp'] def configure(self): self.match_service_name('^snmp') diff --git a/plugins/sslscan.py b/plugins/sslscan.py index 4730238..d9b3f7b 100644 --- a/plugins/sslscan.py +++ b/plugins/sslscan.py @@ -5,7 +5,7 @@ class SSLScan(ServiceScan): def __init__(self): super().__init__() self.name = "SSL Scan" - self.tags = ['default', 'ssl', 'tls'] + self.tags = ['default', 'safe', 'ssl', 'tls'] def configure(self): self.match_all_service_names(True) From b813021267248d1ce82c5dc5b749e6fad5a0bee7 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 23 Aug 2021 20:25:06 -0400 Subject: [PATCH 29/95] Update default-port-scan.py Renamed UDP Port Scan class. Added "long" tag,. --- plugins/default-port-scan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index 8f07637..7ac82f3 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -28,12 +28,12 @@ class AllTCPPortScan(PortScan): await process.wait() return services -class Top20UDPPortScan(PortScan): +class Top100UDPPortScan(PortScan): def __init__(self): super().__init__() self.name = "Top 100 UDP Ports" - self.tags = ["default", "default-port-scan"] + self.tags = ["default", "default-port-scan", "long"] async def run(self, target): # Only run UDP scan if user is root. From 848a0046c164e452ca4f7084d5a78d0514a877b8 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 23 Aug 2021 20:25:19 -0400 Subject: [PATCH 30/95] Update ssh.py Added "safe" tag. --- plugins/ssh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ssh.py b/plugins/ssh.py index bb75718..8c95956 100644 --- a/plugins/ssh.py +++ b/plugins/ssh.py @@ -5,7 +5,7 @@ class NmapSSH(ServiceScan): def __init__(self): super().__init__() self.name = "Nmap SSH" - self.tags = ['default', 'ssh'] + self.tags = ['default', 'safe', 'ssh'] def configure(self): self.match_service_name('^ssh') From aac5e641f18fac9e9856855ec6f4f073fa65c7ec Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 23 Aug 2021 20:25:53 -0400 Subject: [PATCH 31/95] Update rpc.py Added program check to configure. Changed error message in run. --- plugins/rpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/rpc.py b/plugins/rpc.py index 79ef7cf..25a3c1b 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan, error +from autorecon import ServiceScan, error, warn from shutil import which class NmapRPC(ServiceScan): @@ -36,10 +36,12 @@ class RPCDump(ServiceScan): def configure(self): self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) + if which('impacket-rpcdump') is None: + warn('The impacket-rpcdump program could not be found. Some plugins may fail. (On Kali, run: sudo apt install impacket-scripts)') async def run(self, service): if which('impacket-rpcdump') is not None: if service.protocol == 'tcp': await service.execute('impacket-rpcdump -port {port} {address}', outfile='{protocol}_{port}_rpc_rpcdump.txt') else: - error('The impacket-rpcdump program could not be found. Make sure it is installed. (On Kali, run: sudo apt install impacket-scripts)') + error('The impacket-rpcdump program could not be found. (On Kali, run: sudo apt install impacket-scripts)') From cbf8d5ae5abbb00f145f1d0bd9908a7ba5482e8e Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 23 Aug 2021 23:21:14 -0400 Subject: [PATCH 32/95] Added ability to change verbosity by pressing up and down keys. --- autorecon.py | 36 ++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/autorecon.py b/autorecon.py index 8ddac41..40b27a7 100644 --- a/autorecon.py +++ b/autorecon.py @@ -4,9 +4,16 @@ import colorama from typing import final from colorama import Fore, Style import traceback +from pynput import keyboard +import termios, tty colorama.init() +# Save current terminal settings so we can restore them. +terminal_settings = termios.tcgetattr(sys.stdin.fileno()) +# This makes it possible to capture keypresses without and without displaying them. +tty.setcbreak(sys.stdin.fileno()) + class Pattern: def __init__(self, pattern, description=None): @@ -537,6 +544,7 @@ class AutoRecon(object): process = await asyncio.create_subprocess_shell( cmd, + stdin=None, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, executable='/bin/bash') @@ -684,6 +692,9 @@ def cancel_all_tasks(signal, frame): except ProcessLookupError: # Will get raised if the process finishes before we get to killing it. pass + # Restore original terminal settings. + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) + async def start_heartbeat(target, period=60): while True: await asyncio.sleep(period) @@ -701,6 +712,20 @@ async def start_heartbeat(target, period=60): elif count == 1: info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) +def change_verbosity(key): + if key == keyboard.Key.up: + if autorecon.config['verbose'] == 2: + info('Verbosity is already at the highest level.') + else: + autorecon.config['verbose'] += 1 + info('Verbosity increased to ' + str(autorecon.config['verbose'])) + elif key == keyboard.Key.down: + if autorecon.config['verbose'] == 0: + info('Verbosity is already at the lowest level.') + else: + autorecon.config['verbose'] -= 1 + info('Verbosity decreased to ' + str(autorecon.config['verbose'])) + async def port_scan(plugin, target): async with target.autorecon.port_scan_semaphore: info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') @@ -1424,6 +1449,9 @@ async def main(): num_initial_targets = max(1, math.ceil(autorecon.config['max_port_scans'] / port_scan_plugin_count)) + verbosity_monitor = keyboard.Listener(on_press=change_verbosity) + verbosity_monitor.start() + start_time = time.time() pending = [] @@ -1464,6 +1492,14 @@ async def main(): if i >= num_new_targets: break + try: + verbosity_monitor.stop() + except: + pass + + # Restore original terminal settings. + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) + if timed_out: cancel_all_tasks(None, None) diff --git a/requirements.txt b/requirements.txt index d43a59e..30380ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ unidecode toml -colorama \ No newline at end of file +colorama +pynput From bd616f1137c14e38797e42e2ec2fc4665e79a19d Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 24 Aug 2021 16:05:23 -0400 Subject: [PATCH 33/95] Update README.md --- README.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0112ba2..0c18a5f 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,84 @@ See detailed usage options below. ## Usage -TODO +AutoRecon uses Python 3 specific functionality and does not support Python 2. + +``` +usage: autorecon.py [-t TARGET_FILE] [-m MAX_SCANS] [-mp MAX_PORT_SCANS] [-c CONFIG_FILE] [-g GLOBAL_FILE] [--tags TAGS] [--exclude-tags EXCLUDE_TAGS] + [--plugins-dir PLUGINS_DIR] [-o OUTDIR] [--single-target] [--only-scans-dir] [--create-port-dirs] [--heartbeat HEARTBEAT] [--timeout TIMEOUT] + [--target-timeout TARGET_TIMEOUT] [--nmap NMAP | --nmap-append NMAP_APPEND] [--disable-sanity-checks] [--accessible] [-v] [--version] + [--curl.path VALUE] [--dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb}] [--dirbuster.wordlist VALUE] [--dirbuster.threads VALUE] + [--onesixtyone.community-strings VALUE] [--global.username-wordlist VALUE] [--global.password-wordlist VALUE] [--global.domain VALUE] [-h] + [targets ...] + +Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets. + +positional arguments: + targets IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan. + + optional arguments: [30/2643] + -t TARGET_FILE, --targets TARGET_FILE + Read targets from file. + -m MAX_SCANS, --max-scans MAX_SCANS + The maximum number of concurrent scans to run. Default: 50 + -mp MAX_PORT_SCANS, --max-port-scans MAX_PORT_SCANS + The maximum number of concurrent port scans to run. Default: 10 (approx 20% of max-scans unless specified) + -c CONFIG_FILE, --config CONFIG_FILE + Location of AutoRecon's config file. Default: /mnt/hgfs/AutoRecon/config.toml + -g GLOBAL_FILE, --global-file GLOBAL_FILE + Location of AutoRecon's global file. Default: /mnt/hgfs/AutoRecon/global.toml + --tags TAGS Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to + create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: default + --exclude-tags EXCLUDE_TAGS + Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to + create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: None + --plugins-dir PLUGINS_DIR + The location of the plugins directory. Default: /mnt/hgfs/AutoRecon/plugins + -o OUTDIR, --output OUTDIR + The output directory for results. Default: results + --single-target Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the + output directory. Default: False + --only-scans-dir Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: False + --create-port-dirs Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and store results in these directories. Default: False + --heartbeat HEARTBEAT + Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60 + --timeout TIMEOUT Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: None + --target-timeout TARGET_TIMEOUT + Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: None + --nmap NMAP Override the {nmap_extra} variable in scans. Default: -vv --reason -Pn + --nmap-append NMAP_APPEND + Append to the default {nmap_extra} variable in scans. Default: + --disable-sanity-checks + Disable sanity checks that would otherwise prevent the scans from running. Default: False + --accessible Attempts to make AutoRecon output more accessible to screenreaders. Default: False + -v, --verbose Enable verbose output. Repeat for more verbosity. + --version Prints the AutoRecon version and exits. + -h, --help Show this help message and exit. + + plugin arguments: + These are optional arguments for certain plugins. + + --curl.path VALUE The path on the web server to curl. Default: / + --dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb} + The tool to use for directory busting. Default: feroxbuster + --dirbuster.wordlist VALUE + The wordlist to use when directory busting. Specify the option multiple times to use multiple wordlists. Default: + ['/usr/share/seclists/Discovery/Web-Content/common.txt'] + --dirbuster.threads VALUE + The number of threads to use when directory busting. Default: 10 + --onesixtyone.community-strings VALUE + The file containing a list of community strings to try. Default: /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt + + global plugin arguments: + These are optional arguments that can be used by all plugins. + + --global.username-wordlist VALUE + A wordlist of usernames, useful for bruteforcing. Default: /usr/share/seclists/Usernames/top-usernames-shortlist.txt + --global.password-wordlist VALUE + A wordlist of passwords, useful for bruteforcing. Default: /usr/share/seclists/Passwords/darkweb2017-top100.txt + --global.domain VALUE + The domain to use (if known). Used for DNS and/or Active Directory. +``` ### Verbosity @@ -116,6 +193,8 @@ AutoRecon supports three levels of verbosity: * (-v) Verbose output. AutoRecon will additionally specify the exact commands which are being run, as well as highlighting any patterns which are matched in command output. * (-vv) Very verbose output. AutoRecon will output everything. Literally every line from all commands which are currently running. When scanning multiple targets concurrently, this can lead to a ridiculous amount of output. It is not advised to use -vv unless you absolutely need to see live output from commands. +Note: You can change the verbosity of AutoRecon mid-scan by pressing the up and down arrow keys. + ### Results By default, results will be stored in the ./results directory. A new sub directory is created for every target. The structure of this sub directory is: From 226d6ea77d69e45d6f21232a1ad293d3ff596609 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 24 Aug 2021 20:30:13 -0400 Subject: [PATCH 34/95] New functionality. Added a stream readlines() function to read all lines into a list. Added fformat() function, giving plugin authors more access to variables. Fixed "Curl Robots" plugin (suggestion by Alh4zr3d) so it only saves the robots.txt file if it finds one. --- autorecon.py | 31 +++++++++++++++++++++++++++++++ plugins/http.py | 12 ++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/autorecon.py b/autorecon.py index 40b27a7..bcbba97 100644 --- a/autorecon.py +++ b/autorecon.py @@ -227,6 +227,16 @@ class CommandStreamReader(object): else: await asyncio.sleep(0.1) + async def readlines(self): + lines = [] + while True: + line = await self.readline() + if line is not None: + lines.append(line) + else: + break + return lines + class Plugin(object): def __init__(self): @@ -577,6 +587,9 @@ def e(*args, frame_index=1, **kvargs): return string.Formatter().vformat(' '.join(args), args, vals) +def fformat(s): + return e(s, frame_index=3) + def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, **kvargs): frame = sys._getframe(frame_index) @@ -808,7 +821,25 @@ async def service_scan(plugin, service): break async with semaphore: + # Create variables for fformat references. + address = service.target.address + scandir = service.target.scandir + protocol = service.protocol + port = service.port + name = service.name + + # Special cases for HTTP. + http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http' + + nmap_extra = service.target.autorecon.args.nmap + if service.target.autorecon.args.nmap_append: + nmap_extra += ' ' + service.target.autorecon.args.nmap_append + + if protocol == 'udp': + nmap_extra += ' -sU' + tag = service.tag() + '/' + plugin.slug + info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') async with service.target.lock: diff --git a/plugins/http.py b/plugins/http.py index 052e1be..a86b4b8 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan, error +from autorecon import ServiceScan, error, info, fformat from shutil import which import os @@ -67,7 +67,15 @@ class CurlRobots(ServiceScan): async def run(self, service): if service.protocol == 'tcp': - await service.execute('curl -sSik {http_scheme}://{address}:{port}/robots.txt', outfile='{protocol}_{port}_{http_scheme}_curl-robots.txt') + _, stdout, _ = await service.execute('curl -sSikf {http_scheme}://{address}:{port}/robots.txt') + lines = await stdout.readlines() + + if lines: + filename = fformat('{scandir}/{protocol}_{port}_{http_scheme}_curl-robots.txt') + with open(filename, mode='wt', encoding='utf8') as robots: + robots.write('\n'.join(lines)) + else: + info('{bblue}[' + fformat('{tag}') + ']{rst} There did not appear to be a robots.txt file in the webroot (/).') class DirBuster(ServiceScan): From d13ebd4ba4c9f7532ac9a1f08786d680c3273af7 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Thu, 26 Aug 2021 00:38:49 -0400 Subject: [PATCH 35/95] Update dns.py Added Multicast DNS Nmap plugin. --- plugins/dns.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/dns.py b/plugins/dns.py index 50fddfd..850e4e3 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -4,7 +4,7 @@ class DNS(ServiceScan): def __init__(self): super().__init__() - self.name = "DNS" + self.name = 'DNS' self.tags = ['default', 'safe', 'dns'] def configure(self): @@ -17,7 +17,7 @@ class DNSZoneTransfer(ServiceScan): def __init__(self): super().__init__() - self.name = "DNS Zone Transfer" + self.name = 'DNS Zone Transfer' self.tags = ['default', 'safe', 'dns'] def configure(self): @@ -33,7 +33,7 @@ class DNSReverseLookup(ServiceScan): def __init__(self): super().__init__() - self.name = "DNS Reverse Lookup" + self.name = 'DNS Reverse Lookup' self.tags = ['default', 'safe', 'dns'] def configure(self): @@ -41,3 +41,16 @@ class DNSReverseLookup(ServiceScan): async def run(self, service): await service.execute('dig -p {port} -x {address} @{address}', outfile='{protocol}_{port}_dns_reverse-lookup.txt') + +class NmapMulticastDNS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Multicast DNS' + self.tags = ['default', 'safe', 'dns'] + + def configure(self): + self.match_service_name(['^mdns', '^zeroconf']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_multicastdns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_multicastdns_nmap.xml" {address}') From edf47ef097484e696264e04aec644864c7343ef9 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Thu, 26 Aug 2021 01:06:08 -0400 Subject: [PATCH 36/95] Update nfs.py Added mountd plugin. --- plugins/nfs.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/nfs.py b/plugins/nfs.py index b1a96e3..7d7c3f5 100644 --- a/plugins/nfs.py +++ b/plugins/nfs.py @@ -25,3 +25,16 @@ class Showmount(ServiceScan): async def run(self, service): await service.execute('showmount -e {address} 2>&1', outfile='{protocol}_{port}_showmount.txt') + +class NmapMountd(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Mountd" + self.tags = ['default', 'safe', 'nfs'] + + def configure(self): + self.match_service_name('^mountd') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nfs* and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mountd_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mountd_nmap.xml" {address}') From b3b81f5de9c3b8467178fac9e09c9d0abf8231b1 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Thu, 26 Aug 2021 17:10:17 -0400 Subject: [PATCH 37/95] Fixed add_list_option() Previously, add_list_option() used argparse's "append" type. Now it uses nargs='+' to allow multiple options (space separated) that also override defaults rather than appending. --- autorecon.py | 4 ++-- plugins/http.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/autorecon.py b/autorecon.py index bcbba97..17279ec 100644 --- a/autorecon.py +++ b/autorecon.py @@ -267,7 +267,7 @@ class Plugin(object): @final def add_list_option(self, name, default=None, help=None): - self.autorecon.add_argument(self, name, action='append', metavar='VALUE', default=default, help=help) + self.autorecon.add_argument(self, name, nargs='+', metavar='VALUE', default=default, help=help) @final def add_choice_option(self, name, choices, default=None, help=None): @@ -1264,7 +1264,7 @@ async def main(): options.pop('metavar', None) options.pop('default', None) elif gtype == 'list': - options['action'] = 'append' + options['nargs'] = '+' elif gtype == 'choice': if 'choices' not in gvals: fail('Global choice option ' + gkey + ' has no choices value set.') diff --git a/plugins/http.py b/plugins/http.py index a86b4b8..723fe28 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -88,7 +88,7 @@ class DirBuster(ServiceScan): def configure(self): self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') - self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist to use when directory busting. Specify the option multiple times to use multiple wordlists. Default: %(default)s') + self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) From 031afa6003703517d0195f72934463961e0cf539 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Thu, 26 Aug 2021 20:14:06 -0400 Subject: [PATCH 38/95] Bug Fix Fixed a bug where AutoRecon wouldn't restore the terminal after running the help menu in Bash. --- autorecon.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/autorecon.py b/autorecon.py index 17279ec..b4c7a35 100644 --- a/autorecon.py +++ b/autorecon.py @@ -11,8 +11,6 @@ colorama.init() # Save current terminal settings so we can restore them. terminal_settings = termios.tcgetattr(sys.stdin.fileno()) -# This makes it possible to capture keypresses without and without displaying them. -tty.setcbreak(sys.stdin.fileno()) class Pattern: @@ -1480,9 +1478,6 @@ async def main(): num_initial_targets = max(1, math.ceil(autorecon.config['max_port_scans'] / port_scan_plugin_count)) - verbosity_monitor = keyboard.Listener(on_press=change_verbosity) - verbosity_monitor.start() - start_time = time.time() pending = [] @@ -1492,7 +1487,13 @@ async def main(): i+=1 if i >= num_initial_targets: break - + + # This makes it possible to capture keypresses without and without displaying them. + tty.setcbreak(sys.stdin.fileno()) + + verbosity_monitor = keyboard.Listener(on_press=change_verbosity) + verbosity_monitor.start() + timed_out = False while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) From ff8d8de2c8b6b5faa28de571d2533427830ea14b Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 27 Aug 2021 01:29:23 -0400 Subject: [PATCH 39/95] Update autorecon.py Fixed potential memory leak. Fixed possible display bug. --- autorecon.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/autorecon.py b/autorecon.py index 17279ec..df78fbe 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1507,6 +1507,8 @@ async def main(): for task in done: if autorecon.pending_targets: pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + if task in pending: + pending.remove(task) port_scan_task_count = 0 for targ in autorecon.scanning_targets: @@ -1528,9 +1530,6 @@ async def main(): except: pass - # Restore original terminal settings. - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) - if timed_out: cancel_all_tasks(None, None) @@ -1547,6 +1546,9 @@ async def main(): if autorecon.missing_services: warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins based on the service name. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') + # Restore original terminal settings. + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) + if __name__ == '__main__': signal.signal(signal.SIGINT, cancel_all_tasks) try: From a37e1c98c75804e41f7b5139d43c59cd9a81820a Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 27 Aug 2021 03:01:35 -0400 Subject: [PATCH 40/95] Update autorecon.py Added ability to force service scans. --- autorecon.py | 191 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 79 deletions(-) diff --git a/autorecon.py b/autorecon.py index fd643cc..33fc353 100644 --- a/autorecon.py +++ b/autorecon.py @@ -412,6 +412,7 @@ class AutoRecon(object): 'nmap', 'nmap_append', 'disable_sanity_checks', + 'force_services', 'accessible', 'verbose' ] @@ -434,6 +435,7 @@ class AutoRecon(object): 'nmap': '-vv --reason -Pn', 'nmap_append': '', 'disable_sanity_checks': False, + 'force_services': None, 'accessible': False, 'verbose': 0 } @@ -787,36 +789,37 @@ async def service_scan(plugin, service): from autorecon import PortScan semaphore = service.target.autorecon.service_scan_semaphore - # If service scan semaphore is locked, see if we can use port scan semaphore. - while True: - if semaphore.locked(): - if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans + if not service.target.autorecon.config['force_services']: + # If service scan semaphore is locked, see if we can use port scan semaphore. + while True: + if semaphore.locked(): + if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans - port_scan_task_count = 0 - for targ in service.target.autorecon.scanning_targets: - for process_list in targ.running_tasks.values(): - if issubclass(process_list['plugin'].__class__, PortScan): - port_scan_task_count += 1 + port_scan_task_count = 0 + for targ in service.target.autorecon.scanning_targets: + for process_list in targ.running_tasks.values(): + if issubclass(process_list['plugin'].__class__, PortScan): + port_scan_task_count += 1 - if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. - if service.target.autorecon.port_scan_semaphore.locked(): - await asyncio.sleep(1) - continue - semaphore = service.target.autorecon.port_scan_semaphore - break - else: # Do some math to see if we can use the port scan semaphore. - if (service.target.autorecon.config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * service.target.autorecon.config['port_scan_plugin_count']))) >= 1: + if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. if service.target.autorecon.port_scan_semaphore.locked(): await asyncio.sleep(1) continue semaphore = service.target.autorecon.port_scan_semaphore break - else: - await asyncio.sleep(1) + else: # Do some math to see if we can use the port scan semaphore. + if (service.target.autorecon.config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * service.target.autorecon.config['port_scan_plugin_count']))) >= 1: + if service.target.autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = service.target.autorecon.port_scan_semaphore + break + else: + await asyncio.sleep(1) + else: + break else: break - else: - break async with semaphore: # Create variables for fformat references. @@ -917,23 +920,41 @@ async def scan_target(target): heartbeat = asyncio.create_task(start_heartbeat(target, period=target.autorecon.config['heartbeat'])) - for plugin in target.autorecon.plugin_types['port']: - plugin_tag_set = set(plugin.tags) + services = [] + if autorecon.config['force_services']: + forced_services = [x.strip().lower() for x in autorecon.config['force_services']] - matching_tags = False - for tag_group in target.autorecon.tags: - if set(tag_group).issubset(plugin_tag_set): - matching_tags = True - break + for forced_service in forced_services: + match = re.search('(?P(tcp|udp))\/(?P\d+)\/(?P[\w\-\/]+)\/(?Psecure|insecure)', forced_service) + if match: + protocol = match.group('protocol') + port = int(match.group('port')) + service = match.group('service') + secure = True if match.group('secure') == 'secure' else False + service = Service(protocol, port, service, secure) + service.target = target + services.append(service) - excluded_tags = False - for tag_group in target.autorecon.excluded_tags: - if set(tag_group).issubset(plugin_tag_set): - excluded_tags = True - break + if services: + pending.append(asyncio.create_task(asyncio.sleep(0))) + else: + for plugin in target.autorecon.plugin_types['port']: + plugin_tag_set = set(plugin.tags) - if matching_tags and not excluded_tags: - pending.append(asyncio.create_task(port_scan(plugin, target))) + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break + + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break + + if matching_tags and not excluded_tags: + pending.append(asyncio.create_task(port_scan(plugin, target))) async with autorecon.lock: autorecon.scanning_targets.append(target) @@ -953,23 +974,25 @@ async def scan_target(target): timed_out = True break - # Extract Services - services = [] - async with target.lock: - while target.pending_services: - services.append(target.pending_services.pop(0)) + if not autorecon.config['force_services']: + # Extract Services + services = [] - for task in done: - try: - if task.exception(): - print(task.exception()) - continue - except asyncio.InvalidStateError: - pass + async with target.lock: + while target.pending_services: + services.append(target.pending_services.pop(0)) - if task.result()['type'] == 'port': - for service in (task.result()['result'] or []): - services.append(service) + for task in done: + try: + if task.exception(): + print(task.exception()) + continue + except asyncio.InvalidStateError: + pass + + if task.result()['type'] == 'port': + for service in (task.result()['result'] or []): + services.append(service) for service in services: if service.full_tag() not in target.services: @@ -1150,6 +1173,7 @@ async def main(): nmap_group.add_argument('--nmap', action='store', help='Override the {nmap_extra} variable in scans. Default: %(default)s') nmap_group.add_argument('--nmap-append', action='store', help='Append to the default {nmap_extra} variable in scans. Default: %(default)s') parser.add_argument('--disable-sanity-checks', action='store_true', help='Disable sanity checks that would otherwise prevent the scans from running. Default: %(default)s') + parser.add_argument('--force-services', action='store', nargs='+', help='A space separated list of services in the following style: tcp/80/http/insecure tcp/443/https/secure') parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders. Default: %(default)s') parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') parser.add_argument('--version', action='store_true', help='Prints the AutoRecon version and exits.') @@ -1161,7 +1185,7 @@ async def main(): autorecon.argparse = parser if args.version: - print('AutoRecon v2.0-beta1') + print('AutoRecon v2.0-beta2') sys.exit(0) # Parse config file and args for global.toml first. @@ -1332,7 +1356,6 @@ async def main(): if key in autorecon.configurable_boolean_keys and autorecon.config[key]: continue autorecon.config[key] = args_dict[key] - autorecon.args = args if autorecon.config['max_scans'] <= 0: @@ -1367,12 +1390,15 @@ async def main(): errors = True if not errors: - autorecon.port_scan_semaphore = asyncio.Semaphore(autorecon.config['max_port_scans']) - # If max scans and max port scans is the same, the service scan semaphore and port scan semaphore should be the same object - if autorecon.config['max_scans'] == autorecon.config['max_port_scans']: - autorecon.service_scan_semaphore = autorecon.port_scan_semaphore + if autorecon.config['force_services']: + autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans']) else: - autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans'] - autorecon.config['max_port_scans']) + autorecon.port_scan_semaphore = asyncio.Semaphore(autorecon.config['max_port_scans']) + # If max scans and max port scans is the same, the service scan semaphore and port scan semaphore should be the same object + if autorecon.config['max_scans'] == autorecon.config['max_port_scans']: + autorecon.service_scan_semaphore = autorecon.port_scan_semaphore + else: + autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans'] - autorecon.config['max_port_scans']) tags = [] for tag_group in list(set(filter(None, args.tags.lower().split(',')))): @@ -1450,26 +1476,29 @@ async def main(): error('A total of ' + str(len(autorecon.pending_targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.') errors = True - port_scan_plugin_count = 0 - for plugin in autorecon.plugin_types['port']: - matching_tags = False - for tag_group in autorecon.tags: - if set(tag_group).issubset(set(plugin.tags)): - matching_tags = True - break + if not autorecon.config['force_services']: + port_scan_plugin_count = 0 + for plugin in autorecon.plugin_types['port']: + matching_tags = False + for tag_group in autorecon.tags: + if set(tag_group).issubset(set(plugin.tags)): + matching_tags = True + break - excluded_tags = False - for tag_group in autorecon.excluded_tags: - if set(tag_group).issubset(set(plugin.tags)): - excluded_tags = True - break + excluded_tags = False + for tag_group in autorecon.excluded_tags: + if set(tag_group).issubset(set(plugin.tags)): + excluded_tags = True + break - if matching_tags and not excluded_tags: - port_scan_plugin_count += 1 + if matching_tags and not excluded_tags: + port_scan_plugin_count += 1 - if port_scan_plugin_count == 0: - error('There are no port scan plugins that match the tags specified.') - errors = True + if port_scan_plugin_count == 0: + error('There are no port scan plugins that match the tags specified.') + errors = True + else: + port_scan_plugin_count = autorecon.config['max_port_scans'] / 5 if errors: sys.exit(1) @@ -1487,13 +1516,13 @@ async def main(): i+=1 if i >= num_initial_targets: break - + # This makes it possible to capture keypresses without and without displaying them. tty.setcbreak(sys.stdin.fileno()) - + verbosity_monitor = keyboard.Listener(on_press=change_verbosity) verbosity_monitor.start() - + timed_out = False while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) @@ -1514,8 +1543,12 @@ async def main(): port_scan_task_count = 0 for targ in autorecon.scanning_targets: for process_list in targ.running_tasks.values(): - if issubclass(process_list['plugin'].__class__, PortScan): - port_scan_task_count += 1 + if autorecon.config['force_services']: + if issubclass(process_list['plugin'].__class__, ServiceScan): + port_scan_task_count += 1 + else: + if issubclass(process_list['plugin'].__class__, PortScan): + port_scan_task_count += 1 num_new_targets = math.ceil((autorecon.config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count) if num_new_targets > 0: From 7905036688cd77f148fe2d6b90c973121d1e71b5 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 27 Aug 2021 09:31:33 -0400 Subject: [PATCH 41/95] Update autorecon.py Added forced service check. If no services are defined, AutoRecon will error out. --- autorecon.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/autorecon.py b/autorecon.py index 33fc353..effa2b6 100644 --- a/autorecon.py +++ b/autorecon.py @@ -439,6 +439,7 @@ class AutoRecon(object): 'accessible': False, 'verbose': 0 } + self.errors = False self.lock = asyncio.Lock() self.load_slug = None self.load_module = None @@ -937,6 +938,11 @@ async def scan_target(target): if services: pending.append(asyncio.create_task(asyncio.sleep(0))) + else: + error('No services were defined. Please check your service syntax: [tcp|udp]///[secure|insecure]') + heartbeat.cancel() + autorecon.errors = True + return else: for plugin in target.autorecon.plugin_types['port']: plugin_tag_set = set(plugin.tags) @@ -1526,6 +1532,12 @@ async def main(): timed_out = False while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + + # If something failed in scan_target, autorecon.errors will be true. + if autorecon.errors: + cancel_all_tasks(None, None) + sys.exit(1) + # Check if global timeout has occurred. if autorecon.config['timeout'] is not None: elapsed_seconds = round(time.time() - start_time) From 35734efbd0c156533a2d953c8d8b90c9c496909d Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 27 Aug 2021 10:18:44 -0400 Subject: [PATCH 42/95] Update autorecon.py Added new status update feature. Pressing 's' during a scan will print task details. --- autorecon.py | 88 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/autorecon.py b/autorecon.py index effa2b6..cc0da26 100644 --- a/autorecon.py +++ b/autorecon.py @@ -74,7 +74,7 @@ class Target: process, stdout, stderr = await self.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) - target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) + target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) if blocking: while (not (stdout.ended and stderr.ended)): @@ -162,7 +162,7 @@ class Service: process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) - target.running_tasks[tag]['processes'].append({'process':process, 'stderr': stderr, 'cmd': cmd}) + target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) if blocking: while (not (stdout.ended and stderr.ended)): @@ -665,31 +665,43 @@ def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) exit(-1) -def calculate_elapsed_time(start_time): +def calculate_elapsed_time(start_time, short=False): elapsed_seconds = round(time.time() - start_time) m, s = divmod(elapsed_seconds, 60) h, m = divmod(m, 60) elapsed_time = [] - if h == 1: - elapsed_time.append(str(h) + ' hour') - elif h > 1: - elapsed_time.append(str(h) + ' hours') - - if m == 1: - elapsed_time.append(str(m) + ' minute') - elif m > 1: - elapsed_time.append(str(m) + ' minutes') - - if s == 1: - elapsed_time.append(str(s) + ' second') - elif s > 1: - elapsed_time.append(str(s) + ' seconds') + if short: + elapsed_time.append(str(h)) else: - elapsed_time.append('less than a second') + if h == 1: + elapsed_time.append(str(h) + ' hour') + elif h > 1: + elapsed_time.append(str(h) + ' hours') - return ', '.join(elapsed_time) + if short: + elapsed_time.append(str(m)) + else: + if m == 1: + elapsed_time.append(str(m) + ' minute') + elif m > 1: + elapsed_time.append(str(m) + ' minutes') + + if short: + elapsed_time.append(str(s)) + else: + if s == 1: + elapsed_time.append(str(s) + ' second') + elif s > 1: + elapsed_time.append(str(s) + ' seconds') + else: + elapsed_time.append('less than a second') + + if short: + return ':'.join(elapsed_time) + else: + return ', '.join(elapsed_time) def slugify(name): return re.sub(r'[\W_]+', '-', unidecode.unidecode(name).lower()).strip('-') @@ -726,7 +738,7 @@ async def start_heartbeat(target, period=60): elif count == 1: info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) -def change_verbosity(key): +def handle_keyboard(key): if key == keyboard.Key.up: if autorecon.config['verbose'] == 2: info('Verbosity is already at the highest level.') @@ -739,15 +751,36 @@ def change_verbosity(key): else: autorecon.config['verbose'] -= 1 info('Verbosity decreased to ' + str(autorecon.config['verbose'])) + elif key == keyboard.KeyCode.from_char('s'): + for target in autorecon.scanning_targets: + count = len(target.running_tasks) + + tasks_list = [] + if target.autorecon.config['verbose'] >= 1: + for key, value in target.running_tasks.items(): + elapsed_time = calculate_elapsed_time(value['start'], short=True) + tasks_list.append('{bblue}' + key + '{rst}' + ' (elapsed: ' + elapsed_time + ')') + + tasks_list = ':\n ' + '\n '.join(tasks_list) + else: + tasks_list = '' + + current_time = datetime.now().strftime('%H:%M:%S') + + if count > 1: + info('{bgreen}' + current_time + '{rst} - There are {byellow}' + str(count) + '{rst} scans still running against {byellow}' + target.address + '{rst}' + tasks_list) + elif count == 1: + info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) async def port_scan(plugin, target): async with target.autorecon.port_scan_semaphore: info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') - async with target.lock: - target.running_tasks[plugin.slug] = {'plugin': plugin, 'processes':[]} - start_time = time.time() + + async with target.lock: + target.running_tasks[plugin.slug] = {'plugin': plugin, 'processes': [], 'start': start_time} + try: result = await plugin.run(target) except Exception as ex: @@ -844,10 +877,11 @@ async def service_scan(plugin, service): info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') - async with service.target.lock: - service.target.running_tasks[tag] = {'plugin': plugin, 'processes':[]} - start_time = time.time() + + async with service.target.lock: + service.target.running_tasks[tag] = {'plugin': plugin, 'processes': [], 'start': start_time} + try: result = await plugin.run(service) except Exception as ex: @@ -1526,7 +1560,7 @@ async def main(): # This makes it possible to capture keypresses without and without displaying them. tty.setcbreak(sys.stdin.fileno()) - verbosity_monitor = keyboard.Listener(on_press=change_verbosity) + verbosity_monitor = keyboard.Listener(on_press=handle_keyboard) verbosity_monitor.start() timed_out = False From 10e40b2c5315d0a42b37dd84491a1200c0a50ebb Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 27 Aug 2021 15:16:26 -0400 Subject: [PATCH 43/95] Updates & Bug Fixes Updated global option parsing to allow default None values by removing the "default=" setting. Added a match_service() function to ServiceScan plugins to match combinations of protocol/port/name. Fixed bug in status times. Removed defaul from global.domain. Added new WinRM detection plugin. --- autorecon.py | 56 +++++++++++++++++++++++++++++++++++++++++++++---- global.toml | 3 +-- plugins/misc.py | 32 +++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/autorecon.py b/autorecon.py index cc0da26..41904f0 100644 --- a/autorecon.py +++ b/autorecon.py @@ -288,7 +288,13 @@ class Plugin(object): name = 'global.' + slugify(name).replace('-', '_') if name in vars(self.autorecon.args): - return vars(self.autorecon.args)[name] + if vars(self.autorecon.args)[name] is None: + if default: + return default + else: + return None + else: + return vars(self.autorecon.args)[name] else: if default: return default @@ -323,12 +329,42 @@ class ServiceScan(Plugin): super().__init__() self.ports = {'tcp':[], 'udp':[]} self.ignore_ports = {'tcp':[], 'udp':[]} + self.services = [] self.service_names = [] self.ignore_service_names = [] self.match_all_service_names_boolean = False self.run_once_boolean = False self.require_ssl_boolean = False + @final + def match_service(self, protocol, port, name, negative_match=False): + protocol = protocol.lower() + if protocol not in ['tcp', 'udp']: + print('Invalid protocol.') + sys.exit(1) + + if not isinstance(port, list): + port = [port] + + port = list(map(int, port)) + + if not isinstance(name, list): + name = [name] + + valid_regex = True + for r in name: + try: + re.compile(r) + except re.error: + print('Invalid regex: ' + r) + valid_regex = False + + if not valid_regex: + sys.exit(1) + + service = {'protocol': protocol, 'port': port, 'name': name, 'negative_match': negative_match} + self.services.append(service) + @final def match_port(self, protocol, port, negative_match=False): protocol = protocol.lower() @@ -673,7 +709,7 @@ def calculate_elapsed_time(start_time, short=False): elapsed_time = [] if short: - elapsed_time.append(str(h)) + elapsed_time.append(str(h).zfill(2)) else: if h == 1: elapsed_time.append(str(h) + ' hour') @@ -681,7 +717,7 @@ def calculate_elapsed_time(start_time, short=False): elapsed_time.append(str(h) + ' hours') if short: - elapsed_time.append(str(m)) + elapsed_time.append(str(m).zfill(2)) else: if m == 1: elapsed_time.append(str(m) + ' minute') @@ -689,7 +725,7 @@ def calculate_elapsed_time(start_time, short=False): elapsed_time.append(str(m) + ' minutes') if short: - elapsed_time.append(str(s)) + elapsed_time.append(str(s).zfill(2)) else: if s == 1: elapsed_time.append(str(s) + ' second') @@ -1069,6 +1105,18 @@ async def scan_target(target): plugin_service_match = False plugin_tag = service.tag() + '/' + plugin.slug + for service_dict in plugin.services: + if service_dict['protocol'] == protocol and port in service_dict['port']: + for name in service_dict['name']: + if service_dict['negative_match']: + if name not in plugin.ignore_service_names: + plugin.ignore_service_names.append(name) + else: + if name not in plugin.service_names: + plugin.service_names.append(name) + else: + continue + for s in plugin.service_names: if re.search(s, service.name): plugin_service_match = True diff --git a/global.toml b/global.toml index 2ea6ce1..7dbe866 100644 --- a/global.toml +++ b/global.toml @@ -7,5 +7,4 @@ default = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' help = 'A wordlist of passwords, useful for bruteforcing. Default: %(default)s' [global.domain] -default = false -help = 'The domain to use (if known). Used for DNS and/or Active Directory.' +help = 'The domain to use (if known). Used for DNS and/or Active Directory. Default: %(default)s' diff --git a/plugins/misc.py b/plugins/misc.py index 5af0105..f33f95b 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon import ServiceScan, fformat class NmapCassandra(ServiceScan): @@ -142,3 +142,33 @@ class NmapVNC(ServiceScan): async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}') + +class WinRMDetection(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'WinRM Detection' + self.tags = ['default', 'safe', 'winrm'] + + def configure(self): + self.match_service_name('^wsman') + self.match_service('tcp', [5985, 5986], '^http') + + async def run(self, service): + filename = fformat('{scandir}/{protocol}_{port}_winrm-detection.txt') + with open(filename, mode='wt', encoding='utf8') as winrm: + winrm.write('WinRM was possibly detected running on ' + service.protocol + ' port ' + str(service.port) + '.\nCheck _manual_commands.txt for manual commands you can run against this service.') + + def manual(self, service, plugin_was_run): + service.add_manual_commands('Bruteforce logins:', [ + 'crackmapexec winrm {address} -d ' + self.get_global('domain', default='') + ' -u ' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + ' -p ' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + ]) + + service.add_manual_commands('Check login (requires credentials):', [ + 'crackmapexec winrm {address} -d ' + self.get_global('domain', default='') + ' -u -p -x "whoami"' + ]) + + service.add_manual_commands('Evil WinRM (gem install evil-winrm):', [ + 'evil-winrm -u -p -i {address}', + 'evil-winrm -u -H -i {address}' + ]) From 5011fe294fb638a82440ac6590caf55ad745f254 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 28 Aug 2021 08:29:02 -0400 Subject: [PATCH 44/95] Update autorecon.py Added comments. --- autorecon.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/autorecon.py b/autorecon.py index 41904f0..6709ff1 100644 --- a/autorecon.py +++ b/autorecon.py @@ -76,6 +76,7 @@ class Target: target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) + # If process should block, sleep until stdout and stderr have finished. if blocking: while (not (stdout.ended and stderr.ended)): await asyncio.sleep(0.1) @@ -164,6 +165,7 @@ class Service: target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) + # If process should block, sleep until stdout and stderr have finished. if blocking: while (not (stdout.ended and stderr.ended)): await asyncio.sleep(0.1) @@ -182,6 +184,7 @@ class CommandStreamReader(object): self.outfile = outfile self.ended = False + # Read lines from the stream until it ends. async def _read(self): while True: if self.stream.at_eof(): @@ -195,6 +198,8 @@ class CommandStreamReader(object): if self.target.autorecon.config['verbose'] >= 2: if line != '': info('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} ' + line.replace('{', '{{').replace('}', '}}')) + + # Check lines for pattern matches. for p in self.patterns: matches = p.pattern.findall(line) for match in matches: @@ -215,6 +220,7 @@ class CommandStreamReader(object): self.lines.append(line) self.ended = True + # Read a line from the stream cache. async def readline(self): while True: try: @@ -225,6 +231,7 @@ class CommandStreamReader(object): else: await asyncio.sleep(0.1) + # Read all lines from the stream cache. async def readlines(self): lines = [] while True: @@ -1637,8 +1644,9 @@ async def main(): port_scan_task_count = 0 for targ in autorecon.scanning_targets: for process_list in targ.running_tasks.values(): + # If we're not scanning ports, count ServiceScans instead. if autorecon.config['force_services']: - if issubclass(process_list['plugin'].__class__, ServiceScan): + if issubclass(process_list['plugin'].__class__, ServiceScan): # TODO should we really count ServiceScans? Test... port_scan_task_count += 1 else: if issubclass(process_list['plugin'].__class__, PortScan): @@ -1678,6 +1686,7 @@ async def main(): termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) if __name__ == '__main__': + # Capture Ctrl+C and cancel everything. signal.signal(signal.SIGINT, cancel_all_tasks) try: asyncio.run(main()) From db22d1aac5f2ecc21ada591d694af91cbd113e68 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 28 Aug 2021 08:43:11 -0400 Subject: [PATCH 45/95] Update autorecon.py Added notes.txt back. --- autorecon.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/autorecon.py b/autorecon.py index 6709ff1..84ac789 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1085,6 +1085,10 @@ async def scan_target(target): info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') + if not autorecon.config['only_scans_dir']: + with open(os.path.join(target.reportdir, 'notes.txt'), 'a') as file: + file.writelines('[*] ' + service.name + ' found on ' + service.protocol + '/' + str(service.port) + '.\n\n\n\n') + service.target = target # Create variables for command references. From 1d8b7b6e3469bdc60461463bd59afbffca8af5a8 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 28 Aug 2021 09:17:16 -0400 Subject: [PATCH 46/95] Bug Fixes & Improvements Fixed bug where AutoRecon would finish but not output the last few lines. Added new pattern to config file. --- autorecon.py | 12 ++++++++++++ config.toml | 13 +++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/autorecon.py b/autorecon.py index 84ac789..abb966b 100644 --- a/autorecon.py +++ b/autorecon.py @@ -764,6 +764,9 @@ def cancel_all_tasks(signal, frame): # Restore original terminal settings. termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) +def timeout(signal, frame): + raise Exception("Function timed out.") + async def start_heartbeat(target, period=60): while True: await asyncio.sleep(period) @@ -1665,11 +1668,20 @@ async def main(): if i >= num_new_targets: break + # The verbosity_monitor.stop() function sometimes seems to block forever. + # Since it will get killed at the end of the program anyway, if it takes + # more than 1 second to work, we'll time it out. + signal.signal(signal.SIGALRM, timeout) + signal.alarm(1) + try: verbosity_monitor.stop() except: pass + # Cancel the alarm. + signal.alarm(0) + if timed_out: cancel_all_tasks(None, None) diff --git a/config.toml b/config.toml index f343a8d..a2b1a3b 100644 --- a/config.toml +++ b/config.toml @@ -1,7 +1,7 @@ # Configure regular AutoRecon options at the top of this file. # verbose = 1 -# max-scans = 1 +# max-scans = 30 # Configure global pattern matching here. [[pattern]] @@ -11,10 +11,19 @@ pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' [[pattern]] pattern = '(?i)unauthorized' +[[pattern]] +description = 'CVE Identified: {match}' +pattern = '(CVE-\d{4}-\d{4,7})' + # Configure global options here. # [global] # username-wordlist = '/usr/share/seclists/Usernames/cirt-default-usernames.txt' # Configure plugin options here. # [dirbuster] -# wordlist = ['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web-Content/big.txt', '/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt'] +# threads = 50 +# wordlist = [ +# '/usr/share/seclists/Discovery/Web-Content/common.txt', +# '/usr/share/seclists/Discovery/Web-Content/big.txt', +# '/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt' +# ] From 9a30387fab479ab9d3933d76c9ddd85bb16498a0 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 28 Aug 2021 19:08:19 -0400 Subject: [PATCH 47/95] Update autorecon.py Fixed bug in plugin system. --- autorecon.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/autorecon.py b/autorecon.py index abb966b..51923c5 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1318,11 +1318,16 @@ async def main(): for plugin_file in os.listdir(autorecon.config['plugins_dir']): if not plugin_file.startswith('_') and plugin_file.endswith('.py'): - dirname, filename = os.path.split(plugin_file) + dirname, filename = os.path.split(os.path.join(autorecon.config['plugins_dir'], plugin_file)) dirname = os.path.abspath(dirname) + # Temporarily insert the plugin directory into the sys.path so importing plugins works. + sys.path.insert(1, dirname) + try: - plugin = importlib.import_module('.' + filename[:-3], os.path.basename(autorecon.config['plugins_dir'])) + plugin = importlib.import_module(filename[:-3]) + # Remove the plugin directory from the path after import. + sys.path.pop(1) clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) for (_, c) in clsmembers: if c.__module__ == 'autorecon': From eb9a39f3c574aeee173777090b50a9190424c8c0 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 28 Aug 2021 21:41:09 -0400 Subject: [PATCH 48/95] Update http.py Added dirbuster.ext for extensions. Fixed bug in dirsearch command. Removed status codes from gobuster commands. --- plugins/http.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/http.py b/plugins/http.py index 723fe28..da61544 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -81,31 +81,33 @@ class DirBuster(ServiceScan): def __init__(self): super().__init__() - self.name = "DirBuster" + self.name = "Directory Buster" self.slug = 'dirbuster' self.priority = 0 self.tags = ['default', 'safe', 'long', 'http'] def configure(self): self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') - self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') + self.add_list_option('wordlist', default=[os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'wordlists', 'all.txt'))], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') + self.add_option('ext', default='txt,html,php,asp,aspx,jsp', help='The extensions you wish to fuzz (no dot, comma separated). Default: %(default)s') self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) async def run(self, service): + dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "txt,html,php,asp,aspx,jsp" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') + await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x ' + self.get_option('ext') + ' -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') elif self.get_option('tool') == 'gobuster': - await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') + await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') elif self.get_option('tool') == 'dirsearch': - await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w ' + wordlist + ' --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') + await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -e ' + self.get_option('ext') + ' -f -q -w ' + wordlist + ' --format=plain -o "{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') elif self.get_option('tool') == 'ffuf': - await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e ".txt,.html,.php,.asp,.aspx,.jsp" -v | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') + await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') elif self.get_option('tool') == 'dirb': - await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') + await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') def manual(self, service, plugin_was_run): service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ @@ -114,8 +116,8 @@ class DirBuster(ServiceScan): ]) service.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ @@ -129,8 +131,8 @@ class DirBuster(ServiceScan): ]) service.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -s "200,204,301,302,307,403,500" -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) class Nikto(ServiceScan): From 29e285c64f1203cabc0207910a5a7166ef5d916b Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 28 Aug 2021 23:59:01 -0400 Subject: [PATCH 49/95] Added --ports Added ability to scan specific ports. --- autorecon.py | 97 ++++++++++++++++++++++++++++++++++++ plugins/default-port-scan.py | 21 +++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/autorecon.py b/autorecon.py index 51923c5..1b7f1f9 100644 --- a/autorecon.py +++ b/autorecon.py @@ -27,6 +27,7 @@ class Target: self.reportdir = '' self.scandir = '' self.lock = asyncio.Lock() + self.ports = None self.pending_services = [] self.services = [] self.scans = [] @@ -326,6 +327,7 @@ class PortScan(Plugin): def __init__(self): super().__init__() + self.type = None async def run(self, target): raise NotImplementedError @@ -440,6 +442,7 @@ class AutoRecon(object): self.excluded_tags = [] self.patterns = [] self.configurable_keys = [ + 'ports', 'max_scans', 'max_port_scans', 'tags', @@ -463,6 +466,7 @@ class AutoRecon(object): self.config = { 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], 'global_file': os.path.dirname(os.path.realpath(__file__)) + '/global.toml', + 'ports': None, 'max_scans': 50, 'max_port_scans': None, 'tags': 'default', @@ -819,6 +823,23 @@ def handle_keyboard(key): info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) async def port_scan(plugin, target): + if autorecon.config['ports']['tcp'] or autorecon.config['ports']['udp']: + target.ports = {'tcp':None, 'udp':None} + if autorecon.config['ports']['tcp']: + target.ports['tcp'] = ','.join(autorecon.config['ports']['tcp']) + if autorecon.config['ports']['udp']: + target.ports['udp'] = ','.join(autorecon.config['ports']['udp']) + if plugin.type is None: + warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} does not have a type set, and --ports was used. Skipping.') + return {'type':'port', 'plugin':plugin, 'result':[]} + else: + if plugin.type == 'tcp' and not autorecon.config['ports']['tcp']: + warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping') + return {'type':'port', 'plugin':plugin, 'result':[]} + elif plugin.type == 'udp' and not autorecon.config['ports']['udp']: + warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping') + return {'type':'port', 'plugin':plugin, 'result':[]} + async with target.autorecon.port_scan_semaphore: info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') @@ -1257,6 +1278,7 @@ async def main(): parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') + parser.add_argument('-p', '--ports', action='store', type=str, help='Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both TCP/UDP, put port(s) at start or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: %(default)s') parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: %(default)s') parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') @@ -1465,6 +1487,81 @@ async def main(): autorecon.config[key] = args_dict[key] autorecon.args = args + if autorecon.config['ports']: + ports_to_scan = {'tcp':[], 'udp':[]} + unique = {'tcp':[], 'udp':[]} + + ports = autorecon.config['ports'].split(',') + mode = 'both' + for port in ports: + port = port.strip() + if port == '': + continue + + if port.startswith('B:'): + mode = 'both' + port = port.split('B:')[1] + elif port.startswith('T:'): + mode = 'tcp' + port = port.split('T:')[1] + elif port.startswith('U:'): + mode = 'udp' + port = port.split('U:')[1] + + match = re.search('^([0-9]+)\-([0-9]+)$', port) + if match: + num1 = int(match.group(1)) + num2 = int(match.group(2)) + + if num1 > 65535: + fail('Error: A provided port number was too high: ' + str(num1)) + + if num2 > 65535: + fail('Error: A provided port number was too high: ' + str(num2)) + + if num1 == num2: + port_range = [num1] + + if num2 > num1: + port_range = list(range(num1, num2 + 1, 1)) + else: + port_range = list(range(num2, num1 + 1, 1)) + num1 = num1 + num2 + num2 = num1 - num2 + num1 = num1 - num2 + + if mode == 'tcp' or mode == 'both': + for num in port_range: + if num in ports_to_scan['tcp']: + ports_to_scan['tcp'].remove(num) + ports_to_scan['tcp'].append(str(num1) + '-' + str(num2)) + unique['tcp'] = list(set(unique['tcp'] + port_range)) + + if mode == 'udp' or mode == 'both': + for num in port_range: + if num in ports_to_scan['udp']: + ports_to_scan['udp'].remove(num) + ports_to_scan['udp'].append(str(num1) + '-' + str(num2)) + unique['udp'] = list(set(unique['tcp'] + port_range)) + else: + match = re.search('^[0-9]+$', port) + if match: + num = int(port) + + if num > 65535: + fail('Error: A provided port number was too high: ' + str(num)) + + if mode == 'tcp' or mode == 'both': + ports_to_scan['tcp'].append(str(num)) if num not in unique['tcp'] else ports_to_scan['tcp'] + unique['tcp'].append(num) + + if mode == 'udp' or mode == 'both': + ports_to_scan['udp'].append(str(num)) if num not in unique['udp'] else ports_to_scan['udp'] + unique['udp'].append(num) + else: + fail('Error: Invalid port number: ' + str(port)) + autorecon.config['ports'] = ports_to_scan + if autorecon.config['max_scans'] <= 0: error('Argument -m/--max-scans must be at least 1.') errors = True diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index 7ac82f3..0cd812e 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -6,11 +6,18 @@ class QuickTCPPortScan(PortScan): def __init__(self): super().__init__() self.name = "Top TCP Ports" + self.type = 'tcp' self.tags = ["default", "default-port-scan"] self.priority = 0 async def run(self, target): - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + if target.ports: + if target.ports['tcp']: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) + else: + return [] + else: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) services = await target.extract_services(stdout) await process.wait() return services @@ -20,9 +27,12 @@ class AllTCPPortScan(PortScan): def __init__(self): super().__init__() self.name = "All TCP Ports" + self.type = 'tcp' self.tags = ["default", "default-port-scan", "long"] async def run(self, target): + if target.ports: # Don't run this plugin if there are custom ports. + return [] process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) services = await target.extract_services(stdout) await process.wait() @@ -33,12 +43,19 @@ class Top100UDPPortScan(PortScan): def __init__(self): super().__init__() self.name = "Top 100 UDP Ports" + self.type = 'udp' self.tags = ["default", "default-port-scan", "long"] async def run(self, target): # Only run UDP scan if user is root. if os.getuid() == 0: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all --top-ports 100 -oN "{scandir}/_top_20_udp_nmap.txt" -oX "{scandir}/xml/_top_20_udp_nmap.xml" {address}', blocking=False) + if target.ports: + if target.ports['udp']: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all -p ' + target.ports['udp'] + ' -oN "{scandir}/_custom_ports_udp_nmap.txt" -oX "{scandir}/xml/_custom_ports_udp_nmap.xml" {address}', blocking=False) + else: + return [] + else: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all --top-ports 100 -oN "{scandir}/_top_100_udp_nmap.txt" -oX "{scandir}/xml/_top_100_udp_nmap.xml" {address}', blocking=False) services = await target.extract_services(stdout) await process.wait() return services From d84436b6e5ae3f958139d0bdb96acce32e81fc50 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 00:11:04 -0400 Subject: [PATCH 50/95] Added compatibility with Nmap-like comments in target files. Nmap allows comments after the IP / hostname using #. Added the ability to detect and strip out these comments instead of failing. Closes #101 --- autorecon.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/autorecon.py b/autorecon.py index 51923c5..61816e6 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1535,7 +1535,11 @@ async def main(): lines = f.read() for line in lines.splitlines(): line = line.strip() - if line.startswith('#') or len(line) == 0: continue + if line.startswith('#'): continue + match = re.match('([^#]+)#', line) + if match: + line = match.group(1).strip() + if len(line) == 0: continue if line not in raw_targets: raw_targets.append(line) except OSError: From 6d989c4b6296940c4e3e1f0904b836bfcd68e906 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:09:39 -0400 Subject: [PATCH 51/95] Keyboard Control Fixes Added exception handling for required modules. Added ability to disable keyboard controls. Added separate keyboard control module for SSH/Docker (running as root). --- autorecon.py | 82 +++++++++++++++++++++++++++++++++--------------- requirements.txt | 1 + 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/autorecon.py b/autorecon.py index 61816e6..478949b 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1,12 +1,21 @@ -import asyncio, os, sys, re, signal, pkgutil, inspect, importlib, unidecode, argparse, string, ipaddress, socket, toml, time, math +import asyncio, os, re, sys, signal, pkgutil, inspect, importlib, unidecode, argparse, string, ipaddress, socket, time, math from datetime import datetime -import colorama from typing import final from colorama import Fore, Style import traceback -from pynput import keyboard import termios, tty +try: + import colorama, toml + + if os.getuid() == 0: + import keyboard + elif 'DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != ''): + import pynput +except ModuleNotFoundError: + print('One or more required modules was not installed. Please run or re-run: ' + ('sudo ' if os.getuid() == 0 else '') + 'python3 -m pip install -r requirements.txt') + sys.exit(1) + colorama.init() # Save current terminal settings so we can restore them. @@ -455,6 +464,7 @@ class AutoRecon(object): 'nmap', 'nmap_append', 'disable_sanity_checks', + 'disable_keyboard_control', 'force_services', 'accessible', 'verbose' @@ -478,6 +488,7 @@ class AutoRecon(object): 'nmap': '-vv --reason -Pn', 'nmap_append': '', 'disable_sanity_checks': False, + 'disable_keyboard_control': False, 'force_services': None, 'accessible': False, 'verbose': 0 @@ -761,8 +772,10 @@ def cancel_all_tasks(signal, frame): except ProcessLookupError: # Will get raised if the process finishes before we get to killing it. pass - # Restore original terminal settings. - termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) + if not autorecon.config['disable_keyboard_control']: + if os.getuid() == 0 or ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): + # Restore original terminal settings. + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) def timeout(signal, frame): raise Exception("Function timed out.") @@ -785,19 +798,25 @@ async def start_heartbeat(target, period=60): info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) def handle_keyboard(key): - if key == keyboard.Key.up: + # We need to do this to support pynput and keyboard over SSH / Docker. + if os.getuid() != 0: + key_conversion = {pynput.keyboard.Key.up:'up', pynput.keyboard.Key.down:'down', pynput.keyboard.KeyCode.from_char('s'):'s'} + if key in key_conversion.keys(): + key = key_conversion[key] + + if key == 'up': if autorecon.config['verbose'] == 2: info('Verbosity is already at the highest level.') else: autorecon.config['verbose'] += 1 info('Verbosity increased to ' + str(autorecon.config['verbose'])) - elif key == keyboard.Key.down: + elif key == 'down': if autorecon.config['verbose'] == 0: info('Verbosity is already at the lowest level.') else: autorecon.config['verbose'] -= 1 info('Verbosity decreased to ' + str(autorecon.config['verbose'])) - elif key == keyboard.KeyCode.from_char('s'): + elif key == 's': for target in autorecon.scanning_targets: count = len(target.running_tasks) @@ -1275,6 +1294,7 @@ async def main(): nmap_group.add_argument('--nmap', action='store', help='Override the {nmap_extra} variable in scans. Default: %(default)s') nmap_group.add_argument('--nmap-append', action='store', help='Append to the default {nmap_extra} variable in scans. Default: %(default)s') parser.add_argument('--disable-sanity-checks', action='store_true', help='Disable sanity checks that would otherwise prevent the scans from running. Default: %(default)s') + parser.add_argument('--disable-keyboard-control', action='store_true', help='Disables keyboard control ([s]tatus, Up, Down) if you are in SSH or Docker.') parser.add_argument('--force-services', action='store', nargs='+', help='A space separated list of services in the following style: tcp/80/http/insecure tcp/443/https/secure') parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders. Default: %(default)s') parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') @@ -1628,11 +1648,20 @@ async def main(): if i >= num_initial_targets: break - # This makes it possible to capture keypresses without and without displaying them. - tty.setcbreak(sys.stdin.fileno()) + if not autorecon.config['disable_keyboard_control']: + if os.getuid() == 0 or ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): + # This makes it possible to capture keypresses without and without displaying them. + tty.setcbreak(sys.stdin.fileno()) - verbosity_monitor = keyboard.Listener(on_press=handle_keyboard) - verbosity_monitor.start() + if os.getuid() == 0: + keyboard.add_hotkey('s', lambda:handle_keyboard('s')) + keyboard.add_hotkey('up', lambda:handle_keyboard('up')) + keyboard.add_hotkey('down', lambda:handle_keyboard('down')) + else: + keyboard_monitor = pynput.keyboard.Listener(on_press=handle_keyboard) + keyboard_monitor.start() + else: + warn('No "DISPLAY" environment variable was found. This likely means you are in an SSH session or using Docker. Keyboard control features have been disabled. If you run AutoRecon as root you should not have this problem.') timed_out = False while pending: @@ -1677,19 +1706,20 @@ async def main(): if i >= num_new_targets: break - # The verbosity_monitor.stop() function sometimes seems to block forever. - # Since it will get killed at the end of the program anyway, if it takes - # more than 1 second to work, we'll time it out. - signal.signal(signal.SIGALRM, timeout) - signal.alarm(1) + if os.getuid() != 0 and ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): + # The keyboard_monitor.stop() function sometimes seems to block forever. + # Since it will get killed at the end of the program anyway, if it takes + # more than 1 second to work, we'll time it out. + signal.signal(signal.SIGALRM, timeout) + signal.alarm(1) - try: - verbosity_monitor.stop() - except: - pass + try: + keyboard_monitor.stop() + except: + pass - # Cancel the alarm. - signal.alarm(0) + # Cancel the alarm. + signal.alarm(0) if timed_out: cancel_all_tasks(None, None) @@ -1707,8 +1737,10 @@ async def main(): if autorecon.missing_services: warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins based on the service name. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') - # Restore original terminal settings. - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) + if not autorecon.config['disable_keyboard_control']: + if os.getuid() == 0 or ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): + # Restore original terminal settings. + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) if __name__ == '__main__': # Capture Ctrl+C and cancel everything. diff --git a/requirements.txt b/requirements.txt index 30380ae..e4a9216 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ unidecode toml colorama pynput +keyboard From 3c7e1d3c8ec647c97c7677991020ab6d183aa6df Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:10:39 -0400 Subject: [PATCH 52/95] Update autorecon.py Added unidecode to import exception handling. --- autorecon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autorecon.py b/autorecon.py index 478949b..57ae5dc 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1,4 +1,4 @@ -import asyncio, os, re, sys, signal, pkgutil, inspect, importlib, unidecode, argparse, string, ipaddress, socket, time, math +import asyncio, os, re, sys, signal, pkgutil, inspect, importlib, argparse, string, ipaddress, socket, time, math from datetime import datetime from typing import final from colorama import Fore, Style @@ -6,7 +6,7 @@ import traceback import termios, tty try: - import colorama, toml + import colorama, toml, unidecode if os.getuid() == 0: import keyboard From 01e26b67807142d262bc98c064edac130066ecf5 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:11:21 -0400 Subject: [PATCH 53/95] Update autorecon.py Added colorama objects to import exception handling. --- autorecon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorecon.py b/autorecon.py index 57ae5dc..0bec8cd 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1,12 +1,12 @@ import asyncio, os, re, sys, signal, pkgutil, inspect, importlib, argparse, string, ipaddress, socket, time, math from datetime import datetime from typing import final -from colorama import Fore, Style import traceback import termios, tty try: import colorama, toml, unidecode + from colorama import Fore, Style if os.getuid() == 0: import keyboard From ccfedf7db4685917d6f5d9f2ac865cdd1d244f4b Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 15:46:03 -0400 Subject: [PATCH 54/95] Updated Config / Global Files Moved global patterns to the global file. Placed create-port-dirs = true in config file to see if people notice and like it. ;) --- autorecon.py | 27 +++++++++++++-------------- config.toml | 13 +------------ 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/autorecon.py b/autorecon.py index 8956717..5edbdac 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1449,6 +1449,19 @@ async def main(): global_plugin_args = parser.add_argument_group("global plugin arguments", description="These are optional arguments that can be used by all plugins.") global_plugin_args.add_argument('--global.' + slugify(gkey), **options) + elif key == 'pattern' and isinstance(val, list): # Process global patterns. + for pattern in val: + if 'pattern' in pattern: + try: + compiled = re.compile(pattern['pattern']) + if 'description' in pattern: + autorecon.patterns.append(Pattern(compiled, description=pattern['description'])) + else: + autorecon.patterns.append(Pattern(compiled)) + except re.error: + fail('Error: The pattern "' + pattern['pattern'] + '" in the config file is invalid regex.') + else: + fail('Error: A [[pattern]] in the global file doesn\'t have a required pattern variable.') except toml.decoder.TomlDecodeError: fail('Error: Couldn\'t parse ' + g.name + ' file. Check syntax.') @@ -1466,20 +1479,6 @@ async def main(): else: if autorecon.argparse.get_default('global.' + slugify(gkey).replace('-', '_')): autorecon.argparse.set_defaults(**{'global.' + slugify(gkey).replace('-', '_'): gval}) - - elif key == 'pattern' and isinstance(val, list): # Process global patterns. - for pattern in val: - if 'pattern' in pattern: - try: - compiled = re.compile(pattern['pattern']) - if 'description' in pattern: - autorecon.patterns.append(Pattern(compiled, description=pattern['description'])) - else: - autorecon.patterns.append(Pattern(compiled)) - except re.error: - fail('Error: The pattern "' + pattern['pattern'] + '" in the config file is invalid regex.') - else: - fail('Error: A [[pattern]] in the config file doesn\'t have a required pattern variable.') elif isinstance(val, dict): # Process potential plugin arguments. for pkey, pval in config_toml[key].items(): if autorecon.argparse.get_default(slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_')): diff --git a/config.toml b/config.toml index a2b1a3b..0df214e 100644 --- a/config.toml +++ b/config.toml @@ -1,20 +1,9 @@ # Configure regular AutoRecon options at the top of this file. +create-port-dirs = true # verbose = 1 # max-scans = 30 -# Configure global pattern matching here. -[[pattern]] -description = 'Nmap script found a potential vulnerability. ({match})' -pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' - -[[pattern]] -pattern = '(?i)unauthorized' - -[[pattern]] -description = 'CVE Identified: {match}' -pattern = '(CVE-\d{4}-\d{4,7})' - # Configure global options here. # [global] # username-wordlist = '/usr/share/seclists/Usernames/cirt-default-usernames.txt' From 2c62756441d04559f0dfb04f5f283215e379dd2e Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 16:50:16 -0400 Subject: [PATCH 55/95] IPv6 Support Added IPv6 support. If a hostname has an IPv4 address, that will be used. Plugins can either rely on nmap_extra for IPv6 compatibility, or access service.target.type to see if the target address is IPv4 or IPv6. --- autorecon.py | 96 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/autorecon.py b/autorecon.py index 5edbdac..25fcdd5 100644 --- a/autorecon.py +++ b/autorecon.py @@ -29,8 +29,9 @@ class Pattern: class Target: - def __init__(self, address, autorecon): + def __init__(self, address, type, autorecon): self.address = address + self.type = type self.autorecon = autorecon self.basedir = '' self.reportdir = '' @@ -63,6 +64,9 @@ class Target: if self.autorecon.args.nmap_append: nmap_extra += ' ' + self.autorecon.args.nmap_append + if target.type == 'IPv6': + nmap_extra += ' -6' + plugin = inspect.currentframe().f_back.f_locals['self'] cmd = e(cmd) @@ -152,6 +156,9 @@ class Service: if protocol == 'udp': nmap_extra += ' -sU' + if self.target.type == 'IPv6': + nmap_extra += ' -6' + plugin = inspect.currentframe().f_back.f_locals['self'] cmd = e(cmd) @@ -959,6 +966,9 @@ async def service_scan(plugin, service): if protocol == 'udp': nmap_extra += ' -sU' + if service.target.type == 'IPv6': + nmap_extra += ' -6' + tag = service.tag() + '/' + plugin.slug info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') @@ -1150,6 +1160,9 @@ async def scan_target(target): if protocol == 'udp': nmap_extra += ' -sU' + if target.type == 'IPv6': + nmap_extra += ' -6' + service_match = False matching_plugins = [] heading = False @@ -1459,7 +1472,7 @@ async def main(): else: autorecon.patterns.append(Pattern(compiled)) except re.error: - fail('Error: The pattern "' + pattern['pattern'] + '" in the config file is invalid regex.') + fail('Error: The pattern "' + pattern['pattern'] + '" in the global file is invalid regex.') else: fail('Error: A [[pattern]] in the global file doesn\'t have a required pattern variable.') @@ -1664,10 +1677,24 @@ async def main(): for target in raw_targets: try: - ip = str(ipaddress.ip_address(target)) + ip = ipaddress.ip_address(target) + ip_str = str(ip) - if ip not in autorecon.pending_targets: - autorecon.pending_targets.append(ip) + found = False + for t in autorecon.pending_targets: + if t.address == ip_str: + found = True + break + + if found: + continue + + if isinstance(ip, ipaddress.IPv4Address): + autorecon.pending_targets.append(Target(ip_str, 'IPv4', autorecon)) + elif isinstance(ip, ipaddress.IPv6Address): + autorecon.pending_targets.append(Target(ip_str, 'IPv6', autorecon)) + else: + fail('This should never happen unless IPv8 is invented.') except ValueError: try: @@ -1677,19 +1704,56 @@ async def main(): errors = True else: for ip in target_range.hosts(): - ip = str(ip) - if ip not in autorecon.pending_targets: - autorecon.pending_targets.append(ip) + ip_str = str(ip) + + found = False + for t in autorecon.pending_targets: + if t.address == ip_str: + found = True + break + + if found: + continue + + if isinstance(ip, ipaddress.IPv4Address): + autorecon.pending_targets.append(Target(ip_str, 'IPv4', autorecon)) + elif isinstance(ip, ipaddress.IPv6Address): + autorecon.pending_targets.append(Target(ip_str, 'IPv6', autorecon)) + else: + fail('This should never happen unless IPv8 is invented.') + except ValueError: try: - ip = socket.gethostbyname(target) + addresses = socket.getaddrinfo(target, None, socket.AF_INET) - if target not in autorecon.pending_targets: - autorecon.pending_targets.append(target) + found = False + for t in autorecon.pending_targets: + if t.address == target: + found = True + break + + if found: + continue + + autorecon.pending_targets.append(Target(target, 'IPv4', autorecon)) except socket.gaierror: - error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') - errors = True + try: + addresses = socket.getaddrinfo(target, None, socket.AF_INET6) + + found = False + for t in autorecon.pending_targets: + if t.address == target: + found = True + break + + if found: + continue + + autorecon.pending_targets.append(Target(target, 'IPv6', autorecon)) + except socket.gaierror: + error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') + errors = True if len(autorecon.pending_targets) == 0: error('You must specify at least one target to scan!') @@ -1739,7 +1803,7 @@ async def main(): pending = [] i = 0 while autorecon.pending_targets: - pending.append(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + pending.append(asyncio.create_task(scan_target(autorecon.pending_targets.pop(0)))) i+=1 if i >= num_initial_targets: break @@ -1778,7 +1842,7 @@ async def main(): for task in done: if autorecon.pending_targets: - pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + pending.add(asyncio.create_task(scan_target(autorecon.pending_targets.pop(0)))) if task in pending: pending.remove(task) @@ -1797,7 +1861,7 @@ async def main(): if num_new_targets > 0: i = 0 while autorecon.pending_targets: - pending.add(asyncio.create_task(scan_target(Target(autorecon.pending_targets.pop(0), autorecon)))) + pending.add(asyncio.create_task(scan_target(autorecon.pending_targets.pop(0)))) i+=1 if i >= num_new_targets: break From 0a6e364f14059010d97410081cf9cb08f762a171 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 16:58:15 -0400 Subject: [PATCH 56/95] Update http.py Fix for accidental commit. --- plugins/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/http.py b/plugins/http.py index da61544..961bf5c 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -88,7 +88,7 @@ class DirBuster(ServiceScan): def configure(self): self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') - self.add_list_option('wordlist', default=[os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'wordlists', 'all.txt'))], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') + self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') self.add_option('ext', default='txt,html,php,asp,aspx,jsp', help='The extensions you wish to fuzz (no dot, comma separated). Default: %(default)s') self.match_service_name('^http') From fdbf760ef96214bac7ca24797ffec647fa5f124e Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 17:59:38 -0400 Subject: [PATCH 57/95] IPv6 Plugin Support + Bug Fix Added a lot of plugin support for IPv6. Added new {addressv6} template variable. Fixed bug in new --ports feature. Added correct global.toml file. --- autorecon.py | 39 ++++++++++++++++++----------- global.toml | 12 +++++++++ plugins/databases.py | 11 ++++++--- plugins/ftp.py | 4 +-- plugins/http.py | 59 ++++++++++++++++++++++++-------------------- plugins/rdp.py | 4 +-- plugins/rsync.py | 2 +- plugins/sip.py | 3 ++- plugins/smb.py | 19 ++++++++------ plugins/smtp.py | 6 ++--- plugins/snmp.py | 3 ++- plugins/ssh.py | 4 +-- plugins/sslscan.py | 2 +- 13 files changed, 101 insertions(+), 67 deletions(-) diff --git a/autorecon.py b/autorecon.py index 25fcdd5..8cd5543 100644 --- a/autorecon.py +++ b/autorecon.py @@ -58,6 +58,7 @@ class Target: # Create variables for command references. address = target.address + addressv6 = target.address scandir = target.scandir nmap_extra = self.autorecon.args.nmap @@ -66,6 +67,7 @@ class Target: if target.type == 'IPv6': nmap_extra += ' -6' + addressv6 = '[' + addressv6 + ']' plugin = inspect.currentframe().f_back.f_locals['self'] @@ -136,6 +138,7 @@ class Service: # Create variables for command references. address = target.address + addressv6 = target.address scandir = target.scandir protocol = self.protocol port = self.port @@ -158,6 +161,7 @@ class Service: if self.target.type == 'IPv6': nmap_extra += ' -6' + addressv6 = '[' + addressv6 + ']' plugin = inspect.currentframe().f_back.f_locals['self'] @@ -849,22 +853,23 @@ def handle_keyboard(key): info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) async def port_scan(plugin, target): - if autorecon.config['ports']['tcp'] or autorecon.config['ports']['udp']: - target.ports = {'tcp':None, 'udp':None} - if autorecon.config['ports']['tcp']: - target.ports['tcp'] = ','.join(autorecon.config['ports']['tcp']) - if autorecon.config['ports']['udp']: - target.ports['udp'] = ','.join(autorecon.config['ports']['udp']) - if plugin.type is None: - warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} does not have a type set, and --ports was used. Skipping.') - return {'type':'port', 'plugin':plugin, 'result':[]} - else: - if plugin.type == 'tcp' and not autorecon.config['ports']['tcp']: - warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping') - return {'type':'port', 'plugin':plugin, 'result':[]} - elif plugin.type == 'udp' and not autorecon.config['ports']['udp']: - warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping') + if autorecon.config['ports']: + if autorecon.config['ports']['tcp'] or autorecon.config['ports']['udp']: + target.ports = {'tcp':None, 'udp':None} + if autorecon.config['ports']['tcp']: + target.ports['tcp'] = ','.join(autorecon.config['ports']['tcp']) + if autorecon.config['ports']['udp']: + target.ports['udp'] = ','.join(autorecon.config['ports']['udp']) + if plugin.type is None: + warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} does not have a type set, and --ports was used. Skipping.') return {'type':'port', 'plugin':plugin, 'result':[]} + else: + if plugin.type == 'tcp' and not autorecon.config['ports']['tcp']: + warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping') + return {'type':'port', 'plugin':plugin, 'result':[]} + elif plugin.type == 'udp' and not autorecon.config['ports']['udp']: + warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping') + return {'type':'port', 'plugin':plugin, 'result':[]} async with target.autorecon.port_scan_semaphore: info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') @@ -951,6 +956,7 @@ async def service_scan(plugin, service): async with semaphore: # Create variables for fformat references. address = service.target.address + addressv6 = service.target.address scandir = service.target.scandir protocol = service.protocol port = service.port @@ -968,6 +974,7 @@ async def service_scan(plugin, service): if service.target.type == 'IPv6': nmap_extra += ' -6' + addressv6 = '[' + addressv6 + ']' tag = service.tag() + '/' + plugin.slug @@ -1146,6 +1153,7 @@ async def scan_target(target): # Create variables for command references. address = target.address + addressv6 = target.address scandir = target.scandir protocol = service.protocol port = service.port @@ -1162,6 +1170,7 @@ async def scan_target(target): if target.type == 'IPv6': nmap_extra += ' -6' + addressv6 = '[' + addressv6 + ']' service_match = False matching_plugins = [] diff --git a/global.toml b/global.toml index 7dbe866..5ef1da5 100644 --- a/global.toml +++ b/global.toml @@ -8,3 +8,15 @@ help = 'A wordlist of passwords, useful for bruteforcing. Default: %(default)s' [global.domain] help = 'The domain to use (if known). Used for DNS and/or Active Directory. Default: %(default)s' + +# Configure global pattern matching here. +[[pattern]] +description = 'Nmap script found a potential vulnerability. ({match})' +pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' + +[[pattern]] +pattern = '(?i)unauthorized' + +[[pattern]] +description = 'CVE Identified: {match}' +pattern = '(CVE-\d{4}-\d{4,7})' diff --git a/plugins/databases.py b/plugins/databases.py index b45d320..a791ac0 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -24,7 +24,8 @@ class NmapMSSQL(ServiceScan): self.match_service_name(['^mssql', '^ms\-sql']) def manual(self, service, plugin_was_run): - service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + if service.target.type == 'IPv4': + service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}') @@ -40,7 +41,8 @@ class NmapMYSQL(ServiceScan): self.match_service_name('^mysql') def manual(self, service, plugin_was_run): - service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + if service.target.type == 'IPv4': + service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}') @@ -72,8 +74,9 @@ class OracleTNScmd(ServiceScan): self.match_service_name('^oracle') async def run(self, service): - await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') - await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') + if service.target.type == 'IPv4': + await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') + await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') class OracleScanner(ServiceScan): diff --git a/plugins/ftp.py b/plugins/ftp.py index 386a0db..cb9b7ca 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -25,6 +25,6 @@ class BruteforceFTP(ServiceScan): def manual(self, service, plugin_was_run): service.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{address}', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {address}' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{addressv6}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {addressv6}' ]) diff --git a/plugins/http.py b/plugins/http.py index 961bf5c..97b5b89 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -31,10 +31,10 @@ class BruteforceHTTP(ServiceScan): def manual(self, service, plugin_was_run): service.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{address}/path/to/auth/area', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {address} -m DIR:/path/to/auth/area', - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{address}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {address} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{addressv6}/path/to/auth/area', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {addressv6} -m DIR:/path/to/auth/area', + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{addressv6}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {addressv6} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' ]) class Curl(ServiceScan): @@ -52,7 +52,7 @@ class Curl(ServiceScan): async def run(self, service): if service.protocol == 'tcp': - await service.execute('curl -sSik {http_scheme}://{address}:{port}' + self.get_option('path'), outfile='{protocol}_{port}_{http_scheme}_curl.html') + await service.execute('curl -sSik {http_scheme}://{addressv6}:{port}' + self.get_option('path'), outfile='{protocol}_{port}_{http_scheme}_curl.html') class CurlRobots(ServiceScan): @@ -67,7 +67,7 @@ class CurlRobots(ServiceScan): async def run(self, service): if service.protocol == 'tcp': - _, stdout, _ = await service.execute('curl -sSikf {http_scheme}://{address}:{port}/robots.txt') + _, stdout, _ = await service.execute('curl -sSikf {http_scheme}://{addressv6}:{port}/robots.txt') lines = await stdout.readlines() if lines: @@ -99,40 +99,44 @@ class DirBuster(ServiceScan): for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x ' + self.get_option('ext') + ' -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') + await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x ' + self.get_option('ext') + ' -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') elif self.get_option('tool') == 'gobuster': - await service.execute('gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') + await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') elif self.get_option('tool') == 'dirsearch': - await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -e ' + self.get_option('ext') + ' -f -q -w ' + wordlist + ' --format=plain -o "{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') + if service.target.type == 'IPv6': + error('dirsearch does not support IPv6.') + else: + await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -e ' + self.get_option('ext') + ' -f -q -w ' + wordlist + ' --format=plain -o "{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') elif self.get_option('tool') == 'ffuf': - await service.execute('ffuf -u {http_scheme}://{address}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') + await service.execute('ffuf -u {http_scheme}://{addressv6}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') elif self.get_option('tool') == 'dirb': - await service.execute('dirb {http_scheme}://{address}:{port}/ ' + wordlist + ' -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') + await service.execute('dirb {http_scheme}://{addressv6}:{port}/ ' + wordlist + ' -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') def manual(self, service, plugin_was_run): service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'feroxbuster -u {http_scheme}://{address}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', - 'feroxbuster -u {http_scheme}://{address}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' + 'feroxbuster -u {http_scheme}://{addressv6}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', + 'feroxbuster -u {http_scheme}://{addressv6}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' ]) service.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster dir -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + 'gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) - service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', - 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' - ]) + if service.target.type == 'IPv4': + service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', + 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' + ]) service.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirb {http_scheme}://{address}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_big.txt"', - 'dirb {http_scheme}://{address}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' + 'dirb {http_scheme}://{addressv6}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_big.txt"', + 'dirb {http_scheme}://{addressv6}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' ]) service.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + 'gobuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', + 'gobuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' ]) class Nikto(ServiceScan): @@ -147,7 +151,8 @@ class Nikto(ServiceScan): self.match_service_name('^nacn_http$', negative_match=True) def manual(self, service, plugin_was_run): - service.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') + if service.target.type == 'IPv4': + service.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') class WhatWeb(ServiceScan): @@ -161,7 +166,7 @@ class WhatWeb(ServiceScan): self.match_service_name('^nacn_http$', negative_match=True) async def run(self, service): - if service.protocol == 'tcp': + if service.protocol == 'tcp' and service.target.type == 'IPv4': await service.execute('whatweb --color=never --no-errors -a 3 -v {http_scheme}://{address}:{port} 2>&1', outfile='{protocol}_{port}_{http_scheme}_whatweb.txt') class WkHTMLToImage(ServiceScan): @@ -178,7 +183,7 @@ class WkHTMLToImage(ServiceScan): async def run(self, service): if which('wkhtmltoimage') is not None: if service.protocol == 'tcp': - await service.execute('wkhtmltoimage --format png {http_scheme}://{address}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') + await service.execute('wkhtmltoimage --format png {http_scheme}://{addressv6}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') else: error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') @@ -194,4 +199,4 @@ class WPScan(ServiceScan): self.match_service_name('^nacn_http$', negative_match=True) def manual(self, service, plugin_was_run): - service.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{address}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') + service.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{addressv6}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') diff --git a/plugins/rdp.py b/plugins/rdp.py index f075e71..4dfc78e 100644 --- a/plugins/rdp.py +++ b/plugins/rdp.py @@ -25,6 +25,6 @@ class BruteforceRDP(ServiceScan): def manual(self, service, plugin_was_run): service.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{address}', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {address}' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{addressv6}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {addressv6}' ]) diff --git a/plugins/rsync.py b/plugins/rsync.py index 52c02d5..2b4b44e 100644 --- a/plugins/rsync.py +++ b/plugins/rsync.py @@ -24,4 +24,4 @@ class RsyncList(ServiceScan): self.match_service_name('^rsync') async def run(self, service): - await service.execute('rsync -av --list-only rsync://{address}:{port}', outfile='{protocol}_{port}_rsync_file_list.txt') + await service.execute('rsync -av --list-only rsync://{addressv6}:{port}', outfile='{protocol}_{port}_rsync_file_list.txt') diff --git a/plugins/sip.py b/plugins/sip.py index bbbe24c..cbc57f4 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -24,4 +24,5 @@ class SIPVicious(ServiceScan): self.match_service_name('^asterisk') def manual(self, service, plugin_was_run): - service.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') + if service.target.type == 'IPv4': + service.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') diff --git a/plugins/smb.py b/plugins/smb.py index 70ea2ff..d36697d 100644 --- a/plugins/smb.py +++ b/plugins/smb.py @@ -50,7 +50,8 @@ class Enum4Linux(ServiceScan): self.run_once(True) async def run(self, service): - await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') + if service.target.type == 'IPv4': + await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') class NBTScan(ServiceScan): @@ -65,7 +66,8 @@ class NBTScan(ServiceScan): self.run_once(True) async def run(self, service): - await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') + if service.target.type == 'IPv4': + await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') class SMBClient(ServiceScan): @@ -93,9 +95,10 @@ class SMBMap(ServiceScan): self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) async def run(self, service): - await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') - await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') - await service.execute('smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') + if service.target.type == 'IPv4': + await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') diff --git a/plugins/smtp.py b/plugins/smtp.py index 50ec9c5..3ced44e 100644 --- a/plugins/smtp.py +++ b/plugins/smtp.py @@ -24,10 +24,10 @@ class SMTPUserEnum(ServiceScan): self.match_service_name('^smtp') async def run(self, service): - await service.execute('hydra smtp-enum://{address}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') - await service.execute('hydra smtp-enum://{address}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') + await service.execute('hydra smtp-enum://{addressv6}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') + await service.execute('hydra smtp-enum://{addressv6}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') def manual(self, service, plugin_was_run): service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ - 'hydra smtp-enum://{address}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' + 'hydra smtp-enum://{addressv6}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' ]) diff --git a/plugins/snmp.py b/plugins/snmp.py index 2a4ea25..2355cd5 100644 --- a/plugins/snmp.py +++ b/plugins/snmp.py @@ -27,7 +27,8 @@ class OneSixtyOne(ServiceScan): self.add_option('community-strings', default='/usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt', help='The file containing a list of community strings to try. Default: %(default)s') async def run(self, service): - await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') + if service.target.type == 'IPv4': + await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') class SNMPWalk(ServiceScan): diff --git a/plugins/ssh.py b/plugins/ssh.py index 8c95956..aa5d1ec 100644 --- a/plugins/ssh.py +++ b/plugins/ssh.py @@ -25,6 +25,6 @@ class BruteforceSSH(ServiceScan): def manual(self, service, plugin_was_run): service.add_manual_command('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{address}', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {address}' + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{addressv6}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {addressv6}' ]) diff --git a/plugins/sslscan.py b/plugins/sslscan.py index d9b3f7b..88ae41c 100644 --- a/plugins/sslscan.py +++ b/plugins/sslscan.py @@ -13,4 +13,4 @@ class SSLScan(ServiceScan): async def run(self, service): if service.protocol == 'tcp' and service.secure: - await service.execute('sslscan --show-certificate --no-colour {address}:{port} 2>&1', outfile='{protocol}_{port}_sslscan.html') + await service.execute('sslscan --show-certificate --no-colour {addressv6}:{port} 2>&1', outfile='{protocol}_{port}_sslscan.html') From 5115e160b5bd58a3ec5daa47f70da5bac0975b37 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 29 Aug 2021 22:57:30 -0400 Subject: [PATCH 58/95] Update README.md --- README.md | 160 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 89 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 0c18a5f..d02fc34 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,28 @@ AutoRecon was inspired by three tools which the author used during the OSCP labs ## Features -* Supports multiple targets in the form of IP addresses, IP ranges (CIDR notation), and resolvable hostnames. -* Can scan targets concurrently, utilizing multiple processors if they are available. -* Customizable port scanning profiles for flexibility in your initial scans. -* Customizable service enumeration commands and suggested manual follow-up commands. +* Supports multiple targets in the form of IP addresses, IP ranges (CIDR notation), and resolvable hostnames. IPv6 is supported. +* Can scan multiple targets concurrently, utilizing multiple processors if they are available. +* Advanced plugin system allowing for easy creation of new scans. +* Customizable port scanning plugins for flexibility in your initial scans. +* Customizable service scanning plugins for further enumeration. +* Suggested manual follow-up commands for when automation makes little sense. +* Ability to limit port scanning to a combination of TCP/UDP ports. +* Ability to skip port scanning phase by suppling information about services which should be open. +* Global and per-scan pattern matching which highlights and extracts important information from the noise. * An intuitive directory structure for results gathering. * Full logging of commands that were run, along with errors if they fail. -* Global and per-scan pattern matching so you can highlight/extract important information from the noise. +* A powerful config file lets you use your favorite settings every time. +* A tagging system that lets you include or exclude certain plugins. +* Global and per-target timeouts in case you only have limited time. +* Three levels of verbosity, controllable by command-line options, and during scans using Up/Down arrows. +* Colorized output for distinguishing separate pieces of information. Can be turned off for accessibility reasons. ## Requirements - Python 3 - `python3-pip` - ### Supporting packages Several commands used in AutoRecon reference the SecLists project, in the directory /usr/share/seclists/. You can either manually download the SecLists project to this directory (https://github.com/danielmiessler/SecLists), or if you are using Kali Linux (**highly recommended**) you can run the following: @@ -109,11 +117,13 @@ See detailed usage options below. AutoRecon uses Python 3 specific functionality and does not support Python 2. ``` -usage: autorecon.py [-t TARGET_FILE] [-m MAX_SCANS] [-mp MAX_PORT_SCANS] [-c CONFIG_FILE] [-g GLOBAL_FILE] [--tags TAGS] [--exclude-tags EXCLUDE_TAGS] +usage: autorecon.py [-t TARGET_FILE] [-p PORTS] [-m MAX_SCANS] [-mp MAX_PORT_SCANS] [-c CONFIG_FILE] [-g GLOBAL_FILE] [--tags TAGS] [--exclude-tags EXCLUDE_TAGS] [--plugins-dir PLUGINS_DIR] [-o OUTDIR] [--single-target] [--only-scans-dir] [--create-port-dirs] [--heartbeat HEARTBEAT] [--timeout TIMEOUT] - [--target-timeout TARGET_TIMEOUT] [--nmap NMAP | --nmap-append NMAP_APPEND] [--disable-sanity-checks] [--accessible] [-v] [--version] - [--curl.path VALUE] [--dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb}] [--dirbuster.wordlist VALUE] [--dirbuster.threads VALUE] - [--onesixtyone.community-strings VALUE] [--global.username-wordlist VALUE] [--global.password-wordlist VALUE] [--global.domain VALUE] [-h] + [--target-timeout TARGET_TIMEOUT] [--nmap NMAP | --nmap-append NMAP_APPEND] [--disable-sanity-checks] [--disable-keyboard-control] + [--force-services FORCE_SERVICES [FORCE_SERVICES ...]] [--accessible] [-v] [--version] [--curl.path VALUE] + [--dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb}] [--dirbuster.wordlist VALUE [VALUE ...]] [--dirbuster.threads VALUE] + [--dirbuster.ext VALUE] [--onesixtyone.community-strings VALUE] [--global.username-wordlist VALUE] [--global.password-wordlist VALUE] + [--global.domain VALUE] [-h] [targets ...] Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets. @@ -121,68 +131,76 @@ Network reconnaissance tool to port scan and automatically enumerate services fo positional arguments: targets IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan. - optional arguments: [30/2643] - -t TARGET_FILE, --targets TARGET_FILE - Read targets from file. - -m MAX_SCANS, --max-scans MAX_SCANS - The maximum number of concurrent scans to run. Default: 50 - -mp MAX_PORT_SCANS, --max-port-scans MAX_PORT_SCANS - The maximum number of concurrent port scans to run. Default: 10 (approx 20% of max-scans unless specified) - -c CONFIG_FILE, --config CONFIG_FILE - Location of AutoRecon's config file. Default: /mnt/hgfs/AutoRecon/config.toml - -g GLOBAL_FILE, --global-file GLOBAL_FILE - Location of AutoRecon's global file. Default: /mnt/hgfs/AutoRecon/global.toml - --tags TAGS Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to - create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: default - --exclude-tags EXCLUDE_TAGS - Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to - create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: None - --plugins-dir PLUGINS_DIR - The location of the plugins directory. Default: /mnt/hgfs/AutoRecon/plugins - -o OUTDIR, --output OUTDIR - The output directory for results. Default: results - --single-target Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the - output directory. Default: False - --only-scans-dir Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: False - --create-port-dirs Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and store results in these directories. Default: False - --heartbeat HEARTBEAT - Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60 - --timeout TIMEOUT Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: None - --target-timeout TARGET_TIMEOUT - Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: None - --nmap NMAP Override the {nmap_extra} variable in scans. Default: -vv --reason -Pn - --nmap-append NMAP_APPEND - Append to the default {nmap_extra} variable in scans. Default: - --disable-sanity-checks - Disable sanity checks that would otherwise prevent the scans from running. Default: False - --accessible Attempts to make AutoRecon output more accessible to screenreaders. Default: False - -v, --verbose Enable verbose output. Repeat for more verbosity. - --version Prints the AutoRecon version and exits. - -h, --help Show this help message and exit. +optional arguments: + -t TARGET_FILE, --targets TARGET_FILE + Read targets from file. + -p PORTS, --ports PORTS + Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both TCP/UDP, put port(s) at start + or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: None + -m MAX_SCANS, --max-scans MAX_SCANS + The maximum number of concurrent scans to run. Default: 50 + -mp MAX_PORT_SCANS, --max-port-scans MAX_PORT_SCANS + The maximum number of concurrent port scans to run. Default: 10 (approx 20% of max-scans unless specified) + -c CONFIG_FILE, --config CONFIG_FILE + Location of AutoRecon's config file. Default: /mnt/hgfs/AutoRecon/config.toml + -g GLOBAL_FILE, --global-file GLOBAL_FILE + Location of AutoRecon's global file. Default: /mnt/hgfs/AutoRecon/global.toml + --tags TAGS Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to + create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: default + --exclude-tags EXCLUDE_TAGS + Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to + create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: None + --plugins-dir PLUGINS_DIR + The location of the plugins directory. Default: /mnt/hgfs/AutoRecon/plugins + -o OUTDIR, --output OUTDIR + The output directory for results. Default: results + --single-target Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the + output directory. Default: False + --only-scans-dir Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: False + --create-port-dirs Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and store results in these directories. Default: False + --heartbeat HEARTBEAT + Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60 + --timeout TIMEOUT Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: None + --target-timeout TARGET_TIMEOUT + Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: None + --nmap NMAP Override the {nmap_extra} variable in scans. Default: -vv --reason -Pn + --nmap-append NMAP_APPEND + Append to the default {nmap_extra} variable in scans. Default: + --disable-sanity-checks + Disable sanity checks that would otherwise prevent the scans from running. Default: False + --disable-keyboard-control + Disables keyboard control ([s]tatus, Up, Down) if you are in SSH or Docker. + --force-services FORCE_SERVICES [FORCE_SERVICES ...] + A space separated list of services in the following style: tcp/80/http/insecure tcp/443/https/secure + --accessible Attempts to make AutoRecon output more accessible to screenreaders. Default: False + -v, --verbose Enable verbose output. Repeat for more verbosity. + -h, --help Show this help message and exit. - plugin arguments: - These are optional arguments for certain plugins. - - --curl.path VALUE The path on the web server to curl. Default: / - --dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb} - The tool to use for directory busting. Default: feroxbuster - --dirbuster.wordlist VALUE - The wordlist to use when directory busting. Specify the option multiple times to use multiple wordlists. Default: - ['/usr/share/seclists/Discovery/Web-Content/common.txt'] - --dirbuster.threads VALUE - The number of threads to use when directory busting. Default: 10 - --onesixtyone.community-strings VALUE - The file containing a list of community strings to try. Default: /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt - - global plugin arguments: - These are optional arguments that can be used by all plugins. - - --global.username-wordlist VALUE - A wordlist of usernames, useful for bruteforcing. Default: /usr/share/seclists/Usernames/top-usernames-shortlist.txt - --global.password-wordlist VALUE - A wordlist of passwords, useful for bruteforcing. Default: /usr/share/seclists/Passwords/darkweb2017-top100.txt - --global.domain VALUE - The domain to use (if known). Used for DNS and/or Active Directory. +plugin arguments: + These are optional arguments for certain plugins. + + --curl.path VALUE The path on the web server to curl. Default: / + --dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb} + The tool to use for directory busting. Default: feroxbuster + --dirbuster.wordlist VALUE [VALUE ...] + The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: ['/usr/share/seclists/Discovery/Web- + Content/common.txt'] + --dirbuster.threads VALUE + The number of threads to use when directory busting. Default: 10 + --dirbuster.ext VALUE + The extensions you wish to fuzz (no dot, comma separated). Default: txt,html,php,asp,aspx,jsp + --onesixtyone.community-strings VALUE + The file containing a list of community strings to try. Default: /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt + +global plugin arguments: + These are optional arguments that can be used by all plugins. + + --global.username-wordlist VALUE + A wordlist of usernames, useful for bruteforcing. Default: /usr/share/seclists/Usernames/top-usernames-shortlist.txt + --global.password-wordlist VALUE + A wordlist of passwords, useful for bruteforcing. Default: /usr/share/seclists/Passwords/darkweb2017-top100.txt + --global.domain VALUE + The domain to use (if known). Used for DNS and/or Active Directory. Default: None ``` ### Verbosity From b77422d7c0c9fb181166d12a18931fd68dedccf1 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 30 Aug 2021 13:46:26 -0400 Subject: [PATCH 59/95] Update http.py Fixed pattern. --- plugins/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/http.py b/plugins/http.py index 97b5b89..2abad5e 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -48,7 +48,7 @@ class Curl(ServiceScan): self.add_option("path", default="/", help="The path on the web server to curl. Default: %(default)s") self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) - self.add_pattern('(?i)Powered by [^\n]+') + self.add_pattern('(?i)powered[ -]by[^\n]+') async def run(self, service): if service.protocol == 'tcp': From 112268db43b1f63308f2bdff7edab96a0056a1ea Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:51:22 -0400 Subject: [PATCH 60/95] Changed keyboard control code. Changed keyboard control to some custom code which should work with all systems. Unfortunately occasionally it will not register inputs. Unsure why. Good enough for now. --- autorecon.py | 131 +++++++++++++++++++---------------------------- requirements.txt | 2 - 2 files changed, 54 insertions(+), 79 deletions(-) diff --git a/autorecon.py b/autorecon.py index 8cd5543..170bc07 100644 --- a/autorecon.py +++ b/autorecon.py @@ -3,15 +3,11 @@ from datetime import datetime from typing import final import traceback import termios, tty +import select try: import colorama, toml, unidecode from colorama import Fore, Style - - if os.getuid() == 0: - import keyboard - elif 'DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != ''): - import pynput except ModuleNotFoundError: print('One or more required modules was not installed. Please run or re-run: ' + ('sudo ' if os.getuid() == 0 else '') + 'python3 -m pip install -r requirements.txt') sys.exit(1) @@ -196,7 +192,7 @@ class Service: class CommandStreamReader(object): - def __init__(self, stream, target, tag,patterns=None, outfile=None): + def __init__(self, stream, target, tag, patterns=None, outfile=None): self.stream = stream self.target = target self.tag = tag @@ -268,7 +264,6 @@ class Plugin(object): def __init__(self): self.name = None self.slug = None - self.description = None self.tags = ['default'] self.priority = 1 self.patterns = [] @@ -577,9 +572,6 @@ class AutoRecon(object): if plugin is loaded_plugin: fail('Error: plugin "' + plugin.name + '" in ' + filename + ' already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) - if plugin.description is None: - plugin.description = '' - configure_function_found = False run_coroutine_found = False manual_function_found = False @@ -788,9 +780,8 @@ def cancel_all_tasks(signal, frame): pass if not autorecon.config['disable_keyboard_control']: - if os.getuid() == 0 or ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): - # Restore original terminal settings. - termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) + # Restore original terminal settings. + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) def timeout(signal, frame): raise Exception("Function timed out.") @@ -812,45 +803,55 @@ async def start_heartbeat(target, period=60): elif count == 1: info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) -def handle_keyboard(key): - # We need to do this to support pynput and keyboard over SSH / Docker. - if os.getuid() != 0: - key_conversion = {pynput.keyboard.Key.up:'up', pynput.keyboard.Key.down:'down', pynput.keyboard.KeyCode.from_char('s'):'s'} - if key in key_conversion.keys(): - key = key_conversion[key] +async def keyboard(): + input = '' + while True: + if select.select([sys.stdin],[],[],0.1)[0]: + input += sys.stdin.buffer.read1(-1).decode('utf8') + while input != '': + if len(input) >= 3: + if input[:3] == '\x1b[A': + input = '' + if autorecon.config['verbose'] == 2: + info('Verbosity is already at the highest level.') + else: + autorecon.config['verbose'] += 1 + info('Verbosity increased to ' + str(autorecon.config['verbose'])) + elif input[:3] == '\x1b[B': + input = '' + if autorecon.config['verbose'] == 0: + info('Verbosity is already at the lowest level.') + else: + autorecon.config['verbose'] -= 1 + info('Verbosity decreased to ' + str(autorecon.config['verbose'])) + else: + if input[0] != 's': + input = input[1:] - if key == 'up': - if autorecon.config['verbose'] == 2: - info('Verbosity is already at the highest level.') - else: - autorecon.config['verbose'] += 1 - info('Verbosity increased to ' + str(autorecon.config['verbose'])) - elif key == 'down': - if autorecon.config['verbose'] == 0: - info('Verbosity is already at the lowest level.') - else: - autorecon.config['verbose'] -= 1 - info('Verbosity decreased to ' + str(autorecon.config['verbose'])) - elif key == 's': - for target in autorecon.scanning_targets: - count = len(target.running_tasks) + if len(input) > 0 and input[0] == 's': + input = input[1:] + for target in autorecon.scanning_targets: + count = len(target.running_tasks) - tasks_list = [] - if target.autorecon.config['verbose'] >= 1: - for key, value in target.running_tasks.items(): - elapsed_time = calculate_elapsed_time(value['start'], short=True) - tasks_list.append('{bblue}' + key + '{rst}' + ' (elapsed: ' + elapsed_time + ')') + tasks_list = [] + if target.autorecon.config['verbose'] >= 1: + for key, value in target.running_tasks.items(): + elapsed_time = calculate_elapsed_time(value['start'], short=True) + tasks_list.append('{bblue}' + key + '{rst}' + ' (elapsed: ' + elapsed_time + ')') - tasks_list = ':\n ' + '\n '.join(tasks_list) - else: - tasks_list = '' + tasks_list = ':\n ' + '\n '.join(tasks_list) + else: + tasks_list = '' - current_time = datetime.now().strftime('%H:%M:%S') + current_time = datetime.now().strftime('%H:%M:%S') - if count > 1: - info('{bgreen}' + current_time + '{rst} - There are {byellow}' + str(count) + '{rst} scans still running against {byellow}' + target.address + '{rst}' + tasks_list) - elif count == 1: - info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) + if count > 1: + info('{bgreen}' + current_time + '{rst} - There are {byellow}' + str(count) + '{rst} scans still running against {byellow}' + target.address + '{rst}' + tasks_list) + elif count == 1: + info('{bgreen}' + current_time + '{rst} - There is {byellow}1{rst} scan still running against {byellow}' + target.address + '{rst}' + tasks_list) + else: + input = input[1:] + await asyncio.sleep(0.1) async def port_scan(plugin, target): if autorecon.config['ports']: @@ -1078,6 +1079,7 @@ async def scan_target(target): else: error('No services were defined. Please check your service syntax: [tcp|udp]///[secure|insecure]') heartbeat.cancel() + keyboard_monitor.cancel() autorecon.errors = True return else: @@ -1818,19 +1820,8 @@ async def main(): break if not autorecon.config['disable_keyboard_control']: - if os.getuid() == 0 or ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): - # This makes it possible to capture keypresses without and without displaying them. - tty.setcbreak(sys.stdin.fileno()) - - if os.getuid() == 0: - keyboard.add_hotkey('s', lambda:handle_keyboard('s')) - keyboard.add_hotkey('up', lambda:handle_keyboard('up')) - keyboard.add_hotkey('down', lambda:handle_keyboard('down')) - else: - keyboard_monitor = pynput.keyboard.Listener(on_press=handle_keyboard) - keyboard_monitor.start() - else: - warn('No "DISPLAY" environment variable was found. This likely means you are in an SSH session or using Docker. Keyboard control features have been disabled. If you run AutoRecon as root you should not have this problem.') + tty.setcbreak(sys.stdin.fileno()) + keyboard_monitor = asyncio.create_task(keyboard()) timed_out = False while pending: @@ -1875,20 +1866,7 @@ async def main(): if i >= num_new_targets: break - if os.getuid() != 0 and ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): - # The keyboard_monitor.stop() function sometimes seems to block forever. - # Since it will get killed at the end of the program anyway, if it takes - # more than 1 second to work, we'll time it out. - signal.signal(signal.SIGALRM, timeout) - signal.alarm(1) - - try: - keyboard_monitor.stop() - except: - pass - - # Cancel the alarm. - signal.alarm(0) + keyboard_monitor.cancel() if timed_out: cancel_all_tasks(None, None) @@ -1907,9 +1885,8 @@ async def main(): warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins based on the service name. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') if not autorecon.config['disable_keyboard_control']: - if os.getuid() == 0 or ('DISPLAY' in os.environ and (os.environ['DISPLAY'] != None or os.environ['DISPLAY'] != '')): - # Restore original terminal settings. - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) + # Restore original terminal settings. + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) if __name__ == '__main__': # Capture Ctrl+C and cancel everything. diff --git a/requirements.txt b/requirements.txt index e4a9216..d363a63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ unidecode toml colorama -pynput -keyboard From 0ce1770f8424188e89aca70243c442847506a1f9 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 31 Aug 2021 11:10:41 -0400 Subject: [PATCH 61/95] Target handling updates. Changed Target.type to be either 'ip' or 'hostname'. IP version is now stored in Target.ipversion. Updated plugins to reflect this. --- autorecon.py | 23 ++++++++------- plugins/databases.py | 6 ++-- plugins/default-port-scan.py | 1 - plugins/dns.py | 7 +++-- plugins/http.py | 57 +++++++++++++++++------------------- plugins/sip.py | 2 +- plugins/smb.py | 6 ++-- plugins/snmp.py | 2 +- 8 files changed, 51 insertions(+), 53 deletions(-) diff --git a/autorecon.py b/autorecon.py index 170bc07..4a7d5e8 100644 --- a/autorecon.py +++ b/autorecon.py @@ -25,8 +25,9 @@ class Pattern: class Target: - def __init__(self, address, type, autorecon): + def __init__(self, address, ipversion, type, autorecon): self.address = address + self.ipversion = ipversion self.type = type self.autorecon = autorecon self.basedir = '' @@ -61,7 +62,7 @@ class Target: if self.autorecon.args.nmap_append: nmap_extra += ' ' + self.autorecon.args.nmap_append - if target.type == 'IPv6': + if target.ipversion == 'IPv6': nmap_extra += ' -6' addressv6 = '[' + addressv6 + ']' @@ -155,7 +156,7 @@ class Service: if protocol == 'udp': nmap_extra += ' -sU' - if self.target.type == 'IPv6': + if self.target.ipversion == 'IPv6': nmap_extra += ' -6' addressv6 = '[' + addressv6 + ']' @@ -973,7 +974,7 @@ async def service_scan(plugin, service): if protocol == 'udp': nmap_extra += ' -sU' - if service.target.type == 'IPv6': + if service.target.ipversion == 'IPv6': nmap_extra += ' -6' addressv6 = '[' + addressv6 + ']' @@ -1170,7 +1171,7 @@ async def scan_target(target): if protocol == 'udp': nmap_extra += ' -sU' - if target.type == 'IPv6': + if target.ipversion == 'IPv6': nmap_extra += ' -6' addressv6 = '[' + addressv6 + ']' @@ -1701,9 +1702,9 @@ async def main(): continue if isinstance(ip, ipaddress.IPv4Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv4', autorecon)) + autorecon.pending_targets.append(Target(ip_str, 'IPv4', 'ip', autorecon)) elif isinstance(ip, ipaddress.IPv6Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv6', autorecon)) + autorecon.pending_targets.append(Target(ip_str, 'IPv6', 'ip', autorecon)) else: fail('This should never happen unless IPv8 is invented.') except ValueError: @@ -1727,9 +1728,9 @@ async def main(): continue if isinstance(ip, ipaddress.IPv4Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv4', autorecon)) + autorecon.pending_targets.append(Target(ip_str, 'IPv4', 'ip', autorecon)) elif isinstance(ip, ipaddress.IPv6Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv6', autorecon)) + autorecon.pending_targets.append(Target(ip_str, 'IPv6', 'ip', autorecon)) else: fail('This should never happen unless IPv8 is invented.') @@ -1747,7 +1748,7 @@ async def main(): if found: continue - autorecon.pending_targets.append(Target(target, 'IPv4', autorecon)) + autorecon.pending_targets.append(Target(target, 'IPv4', 'hostname', autorecon)) except socket.gaierror: try: addresses = socket.getaddrinfo(target, None, socket.AF_INET6) @@ -1761,7 +1762,7 @@ async def main(): if found: continue - autorecon.pending_targets.append(Target(target, 'IPv6', autorecon)) + autorecon.pending_targets.append(Target(target, 'IPv6', 'hostname', autorecon)) except socket.gaierror: error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') errors = True diff --git a/plugins/databases.py b/plugins/databases.py index a791ac0..e135de4 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -24,7 +24,7 @@ class NmapMSSQL(ServiceScan): self.match_service_name(['^mssql', '^ms\-sql']) def manual(self, service, plugin_was_run): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') async def run(self, service): @@ -41,7 +41,7 @@ class NmapMYSQL(ServiceScan): self.match_service_name('^mysql') def manual(self, service, plugin_was_run): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') async def run(self, service): @@ -74,7 +74,7 @@ class OracleTNScmd(ServiceScan): self.match_service_name('^oracle') async def run(self, service): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index 0cd812e..8f67089 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -27,7 +27,6 @@ class AllTCPPortScan(PortScan): def __init__(self): super().__init__() self.name = "All TCP Ports" - self.type = 'tcp' self.tags = ["default", "default-port-scan", "long"] async def run(self, target): diff --git a/plugins/dns.py b/plugins/dns.py index 850e4e3..e0f59b2 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -25,9 +25,10 @@ class DNSZoneTransfer(ServiceScan): async def run(self, service): if self.get_global('domain'): - await service.execute('dig AXFR -p {port} @{address} ' + self.get_global('domain'), outfile='{protocol}_{port}_dns_zone-transfer.txt') - else: - await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') + await service.execute('dig AXFR -p {port} @{address} ' + self.get_global('domain'), outfile='{protocol}_{port}_dns_zone-transfer-domain.txt') + if service.target.type == 'hostname': + await service.execute('dig AXFR -p {port} @{address} {address}', outfile='{protocol}_{port}_dns_zone-transfer-hostname.txt') + await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') class DNSReverseLookup(ServiceScan): diff --git a/plugins/http.py b/plugins/http.py index 2abad5e..0e08e27 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -88,7 +88,7 @@ class DirBuster(ServiceScan): def configure(self): self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') - self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt'], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') + self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web-Content/big.txt', '/usr/share/seclists/Discovery/Web-Content/raft-large-words.txt'], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') self.add_option('ext', default='txt,html,php,asp,aspx,jsp', help='The extensions you wish to fuzz (no dot, comma separated). Default: %(default)s') self.match_service_name('^http') @@ -99,45 +99,42 @@ class DirBuster(ServiceScan): for wordlist in self.get_option('wordlist'): name = os.path.splitext(os.path.basename(wordlist))[0] if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x ' + self.get_option('ext') + ' -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') + await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') elif self.get_option('tool') == 'gobuster': await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') elif self.get_option('tool') == 'dirsearch': - if service.target.type == 'IPv6': + if service.target.ipversion == 'IPv6': error('dirsearch does not support IPv6.') else: - await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -e ' + self.get_option('ext') + ' -f -q -w ' + wordlist + ' --format=plain -o "{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') + await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -e "' + self.get_option('ext') + '" -f -q -w ' + wordlist + ' --format=plain -o "{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') elif self.get_option('tool') == 'ffuf': await service.execute('ffuf -u {http_scheme}://{addressv6}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') elif self.get_option('tool') == 'dirb': await service.execute('dirb {http_scheme}://{addressv6}:{port}/ ' + wordlist + ' -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') def manual(self, service, plugin_was_run): - service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'feroxbuster -u {http_scheme}://{addressv6}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_big.txt', - 'feroxbuster -u {http_scheme}://{addressv6}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "txt,html,php,asp,aspx,jsp" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' - ]) - - service.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -x "txt,html,php,asp,aspx,jsp" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' - ]) - - if service.target.type == 'IPv4': - service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/seclists/Discovery/Web-Content/big.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_big.txt"', - 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e txt,html,php,asp,aspx,jsp -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' + dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) + if self.get_option('tool') == 'feroxbuster': + service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'feroxbuster -u {http_scheme}://{addressv6}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "' + self.get_option('ext') + '" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' + ]) + elif self.get_option('tool') == 'gobuster': + service.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + 'gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + ]) + elif self.get_option('tool') == 'dirsearch': + if service.target.ipversion == 'IPv4': + service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e "' + self.get_option('ext') + '" -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' + ]) + elif self.get_option('tool') == 'ffuf': + service.add_manual_command('(ffuf) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'ffuf -u {http_scheme}://{addressv6}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_dirbuster.txt' + ]) + elif self.get_option('tool') == 'dirb': + service.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirb {http_scheme}://{addressv6}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' ]) - - service.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirb {http_scheme}://{addressv6}:{port}/ /usr/share/seclists/Discovery/Web-Content/big.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_big.txt"', - 'dirb {http_scheme}://{addressv6}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",.txt,.html,.php,.asp,.aspx,.jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' - ]) - - service.add_manual_command('(gobuster v1 & v2) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/big.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_big.txt"', - 'gobuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -l -x "txt,html,php,asp,aspx,jsp" -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' - ]) class Nikto(ServiceScan): @@ -151,7 +148,7 @@ class Nikto(ServiceScan): self.match_service_name('^nacn_http$', negative_match=True) def manual(self, service, plugin_was_run): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': service.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') class WhatWeb(ServiceScan): @@ -166,7 +163,7 @@ class WhatWeb(ServiceScan): self.match_service_name('^nacn_http$', negative_match=True) async def run(self, service): - if service.protocol == 'tcp' and service.target.type == 'IPv4': + if service.protocol == 'tcp' and service.target.ipversion == 'IPv4': await service.execute('whatweb --color=never --no-errors -a 3 -v {http_scheme}://{address}:{port} 2>&1', outfile='{protocol}_{port}_{http_scheme}_whatweb.txt') class WkHTMLToImage(ServiceScan): diff --git a/plugins/sip.py b/plugins/sip.py index cbc57f4..b69ef25 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -24,5 +24,5 @@ class SIPVicious(ServiceScan): self.match_service_name('^asterisk') def manual(self, service, plugin_was_run): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': service.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') diff --git a/plugins/smb.py b/plugins/smb.py index d36697d..f5c4f65 100644 --- a/plugins/smb.py +++ b/plugins/smb.py @@ -50,7 +50,7 @@ class Enum4Linux(ServiceScan): self.run_once(True) async def run(self, service): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') class NBTScan(ServiceScan): @@ -66,7 +66,7 @@ class NBTScan(ServiceScan): self.run_once(True) async def run(self, service): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') class SMBClient(ServiceScan): @@ -95,7 +95,7 @@ class SMBMap(ServiceScan): self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) async def run(self, service): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') diff --git a/plugins/snmp.py b/plugins/snmp.py index 2355cd5..86a4507 100644 --- a/plugins/snmp.py +++ b/plugins/snmp.py @@ -27,7 +27,7 @@ class OneSixtyOne(ServiceScan): self.add_option('community-strings', default='/usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt', help='The file containing a list of community strings to try. Default: %(default)s') async def run(self, service): - if service.target.type == 'IPv4': + if service.target.ipversion == 'IPv4': await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') class SNMPWalk(ServiceScan): From c2dbc5fbb907009028a1b2e4b9d8a4b2c89d3be9 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 31 Aug 2021 11:41:17 -0400 Subject: [PATCH 62/95] Update autorecon.py Fixed bug where stdin would get passed to Nmap. --- autorecon.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/autorecon.py b/autorecon.py index 4a7d5e8..8d17069 100644 --- a/autorecon.py +++ b/autorecon.py @@ -617,10 +617,9 @@ class AutoRecon(object): process = await asyncio.create_subprocess_shell( cmd, - stdin=None, + stdin=open('/dev/null'), stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - executable='/bin/bash') + stderr=asyncio.subprocess.PIPE) cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile) cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile) From 283f4a725d96b0201933d69e1631a3c926bcbd41 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:31:08 -0400 Subject: [PATCH 63/95] Tagging system updates. Now plugin slugs are included in the tags available, meaning you can run specific plugins using their tag. --- autorecon.py | 10 ++++++++++ plugins/dns.py | 4 ++-- plugins/smb.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/autorecon.py b/autorecon.py index 8d17069..029fc91 100644 --- a/autorecon.py +++ b/autorecon.py @@ -454,6 +454,7 @@ class AutoRecon(object): self.argparse_group = None self.args = None self.missing_services = [] + self.taglist = [] self.tags = [] self.excluded_tags = [] self.patterns = [] @@ -602,6 +603,9 @@ class AutoRecon(object): plugin.tags = [tag.lower() for tag in plugin.tags] + # Add plugin tags to tag list. + [autorecon.taglist.append(t) for t in plugin.tags if t not in autorecon.tags] + plugin.autorecon = self if configure_function_found: plugin.configure() @@ -1413,6 +1417,12 @@ async def main(): print(ex) sys.exit(1) + for plugin in autorecon.plugins.values(): + if plugin.slug in autorecon.taglist: + fail('Plugin ' + plugin.name + ' has a slug (' + plugin.slug + ') with the same name as a tag. Please either change the plugin name or override the slug.') + # Add plugin slug to tags. + plugin.tags += [plugin.slug] + if len(autorecon.plugin_types['port']) == 0: fail('Error: There are no valid PortScan plugins in the plugins directory "' + autorecon.config['plugins_dir'] + '".') diff --git a/plugins/dns.py b/plugins/dns.py index e0f59b2..1f8cfe0 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -1,10 +1,10 @@ from autorecon import ServiceScan -class DNS(ServiceScan): +class NmapDNS(ServiceScan): def __init__(self): super().__init__() - self.name = 'DNS' + self.name = 'Nmap DNS' self.tags = ['default', 'safe', 'dns'] def configure(self): diff --git a/plugins/smb.py b/plugins/smb.py index f5c4f65..af86618 100644 --- a/plugins/smb.py +++ b/plugins/smb.py @@ -41,7 +41,7 @@ class Enum4Linux(ServiceScan): def __init__(self): super().__init__() self.name = "Enum4Linux" - self.tags = ['default', 'safe', 'enum4linux', 'active-directory'] + self.tags = ['default', 'safe', 'active-directory'] def configure(self): self.match_service_name(['^ldap', '^smb', '^microsoft\-ds', '^netbios']) From 0b47b66088bbf20cd76d26e251f82388bc42f6ad Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 31 Aug 2021 23:43:06 -0400 Subject: [PATCH 64/95] Added plugin listing functionality Added -l / --list to list plugins. --- autorecon.py | 13 +++++++++++++ plugins/default-port-scan.py | 15 +++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/autorecon.py b/autorecon.py index 029fc91..95a48fe 100644 --- a/autorecon.py +++ b/autorecon.py @@ -265,6 +265,7 @@ class Plugin(object): def __init__(self): self.name = None self.slug = None + self.description = None self.tags = ['default'] self.priority = 1 self.patterns = [] @@ -1333,6 +1334,7 @@ async def main(): parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--plugins-dir', action='store', type=str, help='The location of the plugins directory. Default: %(default)s') + parser.add_argument('-l', '--list', action='store', nargs='?', const='plugins', help='List all plugins or plugins of a specific type. e.g. --list, --list port, --list service') parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: %(default)s') parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: %(default)s') parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: %(default)s') @@ -1540,6 +1542,17 @@ async def main(): autorecon.config[key] = args_dict[key] autorecon.args = args + if args.list: + type = args.list.lower() + if type in ['plugin', 'plugins', 'port', 'ports', 'portscan', 'portscans']: + for p in autorecon.plugin_types['port']: + print('PortScan: ' + p.name + ' (' + p.slug + ')' + (' - ' + p.description if p.description else '')) + if type in ['plugin', 'plugins', 'service', 'services', 'servicescan', 'servicescans']: + for p in autorecon.plugin_types['service']: + print('ServiceScan: ' + p.name + ' (' + p.slug + ')' + (' - ' + p.description if p.description else '')) + + sys.exit(0) + if autorecon.config['ports']: ports_to_scan = {'tcp':[], 'udp':[]} unique = {'tcp':[], 'udp':[]} diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index 8f67089..6bbaa51 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -5,9 +5,10 @@ class QuickTCPPortScan(PortScan): def __init__(self): super().__init__() - self.name = "Top TCP Ports" + self.name = 'Top TCP Ports' + self.description = 'Performs an Nmap scan of the top 1000 TCP ports.' self.type = 'tcp' - self.tags = ["default", "default-port-scan"] + self.tags = ['default', 'default-port-scan'] self.priority = 0 async def run(self, target): @@ -26,8 +27,9 @@ class AllTCPPortScan(PortScan): def __init__(self): super().__init__() - self.name = "All TCP Ports" - self.tags = ["default", "default-port-scan", "long"] + self.name = 'All TCP Ports' + self.description = 'Performs an Nmap scan of all TCP ports.' + self.tags = ['default', 'default-port-scan', 'long'] async def run(self, target): if target.ports: # Don't run this plugin if there are custom ports. @@ -41,9 +43,10 @@ class Top100UDPPortScan(PortScan): def __init__(self): super().__init__() - self.name = "Top 100 UDP Ports" + self.name = 'Top 100 UDP Ports' + self.description = 'Performs an Nmap scan of the top 100 UDP ports.' self.type = 'udp' - self.tags = ["default", "default-port-scan", "long"] + self.tags = ['default', 'default-port-scan', 'long'] async def run(self, target): # Only run UDP scan if user is root. From 05d49473c18221ed9c07098c8a164711fb81104c Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 1 Sep 2021 13:18:09 -0400 Subject: [PATCH 65/95] New Features Added ability to add an additional plugins directory instead of overriding the original. Useful for plugin dev. Also added a new non-default port scan which guesses services based on open ports. --- autorecon.py | 73 +++++++++++++++++++++++--------------- plugins/guess-port-scan.py | 47 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 plugins/guess-port-scan.py diff --git a/autorecon.py b/autorecon.py index 95a48fe..6584f0e 100644 --- a/autorecon.py +++ b/autorecon.py @@ -466,6 +466,7 @@ class AutoRecon(object): 'tags', 'exclude_tags', 'plugins_dir', + 'add_plugins-dir', 'outdir', 'single_target', 'only_scans_dir', @@ -491,6 +492,7 @@ class AutoRecon(object): 'tags': 'default', 'exclude_tags': None, 'plugins_dir': os.path.dirname(os.path.abspath(__file__)) + '/plugins', + 'add_plugins_dir': None, 'outdir': 'results', 'single_target': False, 'only_scans_dir': False, @@ -1334,6 +1336,7 @@ async def main(): parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--plugins-dir', action='store', type=str, help='The location of the plugins directory. Default: %(default)s') + parser.add_argument('--add-plugins-dir', action='store', type=str, help='The location of an additional plugins directory to add to the main one. Default: %(default)s') parser.add_argument('-l', '--list', action='store', nargs='?', const='plugins', help='List all plugins or plugins of a specific type. e.g. --list, --list port, --list service') parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: %(default)s') parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: %(default)s') @@ -1370,54 +1373,68 @@ async def main(): try: config_toml = toml.load(c) for key, val in config_toml.items(): + key = slugify(key) if key == 'global-file': autorecon.config['global_file'] = val elif key == 'plugins-dir': autorecon.config['plugins_dir'] = val + elif key == 'add-plugins-dir': + autorecon.config['add_plugins_dir'] = val except toml.decoder.TomlDecodeError: fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.') args_dict = vars(args) for key in args_dict: - if key == 'global_file' and args_dict['global_file'] is not None: + key = slugify(key) + if key == 'global-file' and args_dict['global_file'] is not None: autorecon.config['global_file'] = args_dict['global_file'] - elif key == 'plugins_dir' and args_dict['plugins_dir'] is not None: + elif key == 'plugins-dir' and args_dict['plugins_dir'] is not None: autorecon.config['plugins_dir'] = args_dict['plugins_dir'] + elif key == 'add-plugins-dir' and args_dict['add_plugins_dir'] is not None: + autorecon.config['add_plugins_dir'] = args_dict['add_plugins_dir'] if not os.path.isdir(autorecon.config['plugins_dir']): fail('Error: Specified plugins directory "' + autorecon.config['plugins_dir'] + '" does not exist.') - for plugin_file in os.listdir(autorecon.config['plugins_dir']): - if not plugin_file.startswith('_') and plugin_file.endswith('.py'): + if autorecon.config['add_plugins_dir'] and not os.path.isdir(autorecon.config['add_plugins_dir']): + fail('Error: Specified additional plugins directory "' + autorecon.config['add_plugins_dir'] + '" does not exist.') - dirname, filename = os.path.split(os.path.join(autorecon.config['plugins_dir'], plugin_file)) - dirname = os.path.abspath(dirname) + plugins_dirs = [autorecon.config['plugins_dir']] + if autorecon.config['add_plugins_dir']: + plugins_dirs.append(autorecon.config['add_plugins_dir']) - # Temporarily insert the plugin directory into the sys.path so importing plugins works. - sys.path.insert(1, dirname) + for plugins_dir in plugins_dirs: + for plugin_file in os.listdir(plugins_dir): + if not plugin_file.startswith('_') and plugin_file.endswith('.py'): - try: - plugin = importlib.import_module(filename[:-3]) - # Remove the plugin directory from the path after import. - sys.path.pop(1) - clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) - for (_, c) in clsmembers: - if c.__module__ == 'autorecon': - continue + dirname, filename = os.path.split(os.path.join(plugins_dir, plugin_file)) + dirname = os.path.abspath(dirname) - if c.__name__.lower() in autorecon.config['protected_classes']: - print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.') - sys.exit(1) + # Temporarily insert the plugin directory into the sys.path so importing plugins works. + sys.path.insert(1, dirname) - # Only add classes that are a sub class of either PortScan or ServiceScan - if issubclass(c, PortScan) or issubclass(c, ServiceScan): - autorecon.register(c(), filename) - else: - print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.') - except (ImportError, SyntaxError) as ex: - print('cannot import ' + filename + ' plugin') - print(ex) - sys.exit(1) + try: + plugin = importlib.import_module(filename[:-3]) + # Remove the plugin directory from the path after import. + sys.path.pop(1) + clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) + for (_, c) in clsmembers: + if c.__module__ == 'autorecon': + continue + + if c.__name__.lower() in autorecon.config['protected_classes']: + print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.') + sys.exit(1) + + # Only add classes that are a sub class of either PortScan or ServiceScan + if issubclass(c, PortScan) or issubclass(c, ServiceScan): + autorecon.register(c(), filename) + else: + print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.') + except (ImportError, SyntaxError) as ex: + print('cannot import ' + filename + ' plugin') + print(ex) + sys.exit(1) for plugin in autorecon.plugins.values(): if plugin.slug in autorecon.taglist: diff --git a/plugins/guess-port-scan.py b/plugins/guess-port-scan.py new file mode 100644 index 0000000..89ad72a --- /dev/null +++ b/plugins/guess-port-scan.py @@ -0,0 +1,47 @@ +from autorecon import PortScan, Service +import re + +class GuesPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = 'Guess TCP Ports' + self.type = 'tcp' + self.description = 'Performs an Nmap scan of the all TCP ports but guesses services based off the port found. Can be quicker. Proper service matching is performed at the end of the scan.' + self.tags = ['guess-port-scan', 'long'] + self.priority = 0 + + async def run(self, target): + if target.ports: + if target.ports['tcp']: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) + else: + return [] + else: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + + insecure_ports = { + '20':'ftp', '21':'ftp', '22':'ssh', '23':'telnet', '25':'smtp', '53':'domain', '69':'tftp', '79':'finger', '80':'http', '88':'kerberos', '109':'pop3', '110':'pop3', '111':'rpcbind', '119':'nntp', '135':'msrpc', '139':'netbios-ssn', '143':'imap', '161':'snmp', '220':'imap', '389':'ldap', '433':'nntp', '445':'smb', '587':'smtp', '631':'ipp', '873':'rsync', '1098':'java-rmi', '1099':'java-rmi', '1433':'mssql', '1521':'oracle', '2049':'nfs', '2483':'oracle', '3020':'smb', '3306':'mysql', '3389':'rdp', '3632':'distccd', '5060':'asterisk', '5500':'vnc', '5900':'vnc', '5985':'wsman', '6379':'redis', '8080':'http-proxy', '27017':'mongod', '27018':'mongod', '27019':'mongod' + } + secure_ports = { + '443':'https', '465':'smtp', '563':'nntp', '585':'imaps', '593':'msrpc', '636':'ldap', '989':'ftp', '990':'ftp', '992':'telnet', '993':'imaps', '995':'pop3s', '2484':'oracle', '5061':'asterisk', '5986':'wsman' + } + + services = [] + while True: + line = await stdout.readline() + if line is not None: + match = re.match('^Discovered open port ([0-9]+)/tcp', line) + if match: + if match.group(1) in insecure_ports.keys(): + await target.add_service(Service('tcp', match.group(1), insecure_ports[match.group(1)])) + elif match.group(1) in secure_ports.keys(): + await target.add_service(Service('tcp', match.group(1), secure_ports[match.group(1)], True)) + service = target.extract_service(line) + if service is not None: + services.append(service) + else: + break + + await process.wait() + return services From b4f688b5a752b015576f55e3b96b0ad59c1a4aaa Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 1 Sep 2021 14:48:21 -0400 Subject: [PATCH 66/95] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d02fc34..a1aa5f7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ AutoRecon was inspired by three tools which the author used during the OSCP labs ## Features -* Supports multiple targets in the form of IP addresses, IP ranges (CIDR notation), and resolvable hostnames. IPv6 is supported. +* Supports multiple targets in the form of IP addresses, IP ranges (CIDR notation), and resolvable hostnames. IPv6 is also supported. * Can scan multiple targets concurrently, utilizing multiple processors if they are available. * Advanced plugin system allowing for easy creation of new scans. * Customizable port scanning plugins for flexibility in your initial scans. From 6e81dc8431d6eed9527c90b8fdbf75d437eccf4e Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 1 Sep 2021 23:52:16 -0400 Subject: [PATCH 67/95] Refactoring codebase. Moved most of the core functionality to modules. Plugins updates with new module name. --- autorecon.py | 955 +++++------------------------------ autorecon/__init__.py | 0 autorecon/config.py | 59 +++ autorecon/io.py | 163 ++++++ autorecon/plugins.py | 337 ++++++++++++ autorecon/targets.py | 172 +++++++ plugins/databases.py | 2 +- plugins/default-port-scan.py | 3 +- plugins/dns.py | 2 +- plugins/ftp.py | 2 +- plugins/guess-port-scan.py | 3 +- plugins/http.py | 3 +- plugins/kerberos.py | 2 +- plugins/ldap.py | 2 +- plugins/misc.py | 3 +- plugins/nfs.py | 2 +- plugins/rdp.py | 2 +- plugins/redis.py | 3 +- plugins/rpc.py | 3 +- plugins/rsync.py | 2 +- plugins/sip.py | 2 +- plugins/smb.py | 2 +- plugins/smtp.py | 2 +- plugins/snmp.py | 2 +- plugins/ssh.py | 2 +- plugins/sslscan.py | 2 +- 26 files changed, 878 insertions(+), 854 deletions(-) create mode 100644 autorecon/__init__.py create mode 100644 autorecon/config.py create mode 100644 autorecon/io.py create mode 100644 autorecon/plugins.py create mode 100644 autorecon/targets.py diff --git a/autorecon.py b/autorecon.py index 6584f0e..664792c 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1,9 +1,5 @@ -import asyncio, os, re, sys, signal, pkgutil, inspect, importlib, argparse, string, ipaddress, socket, time, math +import argparse, asyncio, importlib, inspect, ipaddress, math, os, re, sys, signal, select, socket, termios, time, traceback, tty from datetime import datetime -from typing import final -import traceback -import termios, tty -import select try: import colorama, toml, unidecode @@ -14,724 +10,15 @@ except ModuleNotFoundError: colorama.init() +from autorecon.config import config, configurable_keys, configurable_boolean_keys +from autorecon.io import slugify, e, fformat, cprint, debug, info, warn, error, fail, CommandStreamReader +from autorecon.plugins import Pattern, PortScan, ServiceScan, AutoRecon +from autorecon.targets import Target, Service + # Save current terminal settings so we can restore them. terminal_settings = termios.tcgetattr(sys.stdin.fileno()) -class Pattern: - - def __init__(self, pattern, description=None): - self.pattern = pattern - self.description = description - -class Target: - - def __init__(self, address, ipversion, type, autorecon): - self.address = address - self.ipversion = ipversion - self.type = type - self.autorecon = autorecon - self.basedir = '' - self.reportdir = '' - self.scandir = '' - self.lock = asyncio.Lock() - self.ports = None - self.pending_services = [] - self.services = [] - self.scans = [] - self.running_tasks = {} - - async def add_service(self, service): - async with self.lock: - self.pending_services.append(service) - - def extract_service(self, line, regex=None): - return self.autorecon.extract_service(line, regex) - - async def extract_services(self, stream, regex=None): - return await self.autorecon.extract_services(stream, regex) - - async def execute(self, cmd, blocking=True, outfile=None, errfile=None): - target = self - - # Create variables for command references. - address = target.address - addressv6 = target.address - scandir = target.scandir - - nmap_extra = self.autorecon.args.nmap - if self.autorecon.args.nmap_append: - nmap_extra += ' ' + self.autorecon.args.nmap_append - - if target.ipversion == 'IPv6': - nmap_extra += ' -6' - addressv6 = '[' + addressv6 + ']' - - plugin = inspect.currentframe().f_back.f_locals['self'] - - cmd = e(cmd) - - tag = plugin.slug - - if target.autorecon.config['verbose'] >= 1: - info('Port scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) - - if outfile is not None: - outfile = os.path.join(target.scandir, e(outfile)) - - if errfile is not None: - errfile = os.path.join(target.scandir, e(errfile)) - - async with target.lock: - with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: - file.writelines(cmd + '\n\n') - - process, stdout, stderr = await self.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) - - target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) - - # If process should block, sleep until stdout and stderr have finished. - if blocking: - while (not (stdout.ended and stderr.ended)): - await asyncio.sleep(0.1) - await process.wait() - - return process, stdout, stderr - -class Service: - - def __init__(self, protocol, port, name, secure=False): - self.target = None - self.protocol = protocol.lower() - self.port = int(port) - self.name = name - self.secure = secure - self.manual_commands = {} - - @final - def tag(self): - return self.protocol + '/' + str(self.port) + '/' + self.name - - @final - def full_tag(self): - return self.protocol + '/' + str(self.port) + '/' + self.name + '/' + ('secure' if self.secure else 'insecure') - - @final - def add_manual_commands(self, description, commands): - if not isinstance(commands, list): - commands = [commands] - if description not in self.manual_commands: - self.manual_commands[description] = [] - - # Merge in new unique commands, while preserving order. - [self.manual_commands[description].append(m) for m in commands if m not in self.manual_commands[description]] - - @final - def add_manual_command(self, description, command): - self.add_manual_commands(description, command) - - @final - async def execute(self, cmd, blocking=True, outfile=None, errfile=None): - target = self.target - - # Create variables for command references. - address = target.address - addressv6 = target.address - scandir = target.scandir - protocol = self.protocol - port = self.port - name = self.name - - if target.autorecon.config['create_port_dirs']: - scandir = os.path.join(scandir, protocol + str(port)) - os.makedirs(scandir, exist_ok=True) - os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) - - # Special cases for HTTP. - http_scheme = 'https' if 'https' in self.name or self.secure is True else 'http' - - nmap_extra = self.target.autorecon.args.nmap - if self.target.autorecon.args.nmap_append: - nmap_extra += ' ' + self.target.autorecon.args.nmap_append - - if protocol == 'udp': - nmap_extra += ' -sU' - - if self.target.ipversion == 'IPv6': - nmap_extra += ' -6' - addressv6 = '[' + addressv6 + ']' - - plugin = inspect.currentframe().f_back.f_locals['self'] - - cmd = e(cmd) - - tag = self.tag() + '/' + plugin.slug - - if target.autorecon.config['verbose'] >= 1: - info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) - - if outfile is not None: - outfile = os.path.join(scandir, e(outfile)) - - if errfile is not None: - errfile = os.path.join(scandir, e(errfile)) - - async with target.lock: - with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: - file.writelines(cmd + '\n\n') - - process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) - - target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) - - # If process should block, sleep until stdout and stderr have finished. - if blocking: - while (not (stdout.ended and stderr.ended)): - await asyncio.sleep(0.1) - await process.wait() - - return process, stdout, stderr - -class CommandStreamReader(object): - - def __init__(self, stream, target, tag, patterns=None, outfile=None): - self.stream = stream - self.target = target - self.tag = tag - self.lines = [] - self.patterns = patterns or [] - self.outfile = outfile - self.ended = False - - # Read lines from the stream until it ends. - async def _read(self): - while True: - if self.stream.at_eof(): - break - try: - line = (await self.stream.readline()).decode('utf8').rstrip() - except ValueError: - error('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} A line was longer than 64 KiB and cannot be processed. Ignoring.') - continue - - if self.target.autorecon.config['verbose'] >= 2: - if line != '': - info('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} ' + line.replace('{', '{{').replace('}', '}}')) - - # Check lines for pattern matches. - for p in self.patterns: - matches = p.pattern.findall(line) - for match in matches: - async with self.target.lock: - with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file: - if p.description: - if self.target.autorecon.config['verbose'] >= 1: - info('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} {bmagenta}' + p.description.replace('{match}', '{bblue}' + match + '{crst}{bmagenta}') + '{rst}') - file.writelines(p.description.replace('{match}', match) + '\n\n') - else: - if self.target.autorecon.config['verbose'] >= 1: - info('{bblue}[' + self.target.address + '/' + self.tag + ']{crst} {bmagenta}Matched Pattern: {bblue}' + match + '{rst}') - file.writelines('Matched Pattern: ' + match + '\n\n') - - if self.outfile is not None: - with open(self.outfile, 'a') as writer: - writer.write(line + '\n') - self.lines.append(line) - self.ended = True - - # Read a line from the stream cache. - async def readline(self): - while True: - try: - return self.lines.pop(0) - except IndexError: - if self.ended: - return None - else: - await asyncio.sleep(0.1) - - # Read all lines from the stream cache. - async def readlines(self): - lines = [] - while True: - line = await self.readline() - if line is not None: - lines.append(line) - else: - break - return lines - -class Plugin(object): - - def __init__(self): - self.name = None - self.slug = None - self.description = None - self.tags = ['default'] - self.priority = 1 - self.patterns = [] - self.autorecon = None - self.disabled = False - - @final - def add_option(self, name, default=None, help=None): - self.autorecon.add_argument(self, name, metavar='VALUE', default=default, help=help) - - @final - def add_constant_option(self, name, const, default=None, help=None): - self.autorecon.add_argument(self, name, action='store_const', const=const, default=default, help=help) - - @final - def add_true_option(self, name, help=None): - self.autorecon.add_argument(self, name, action='store_true', help=help) - - @final - def add_false_option(self, name, help=None): - self.autorecon.add_argument(self, name, action='store_false', help=help) - - @final - def add_list_option(self, name, default=None, help=None): - self.autorecon.add_argument(self, name, nargs='+', metavar='VALUE', default=default, help=help) - - @final - def add_choice_option(self, name, choices, default=None, help=None): - if not isinstance(choices, list): - fail('The choices argument for ' + self.name + '\'s ' + name + ' choice option should be a list.') - self.autorecon.add_argument(self, name, choices=choices, default=default, help=help) - - @final - def get_option(self, name): - # TODO: make sure name is simple. - name = self.slug.replace('-', '_') + '.' + slugify(name).replace('-', '_') - - if name in vars(self.autorecon.args): - return vars(self.autorecon.args)[name] - else: - return None - - @final - def get_global_option(self, name, default=None): - name = 'global.' + slugify(name).replace('-', '_') - - if name in vars(self.autorecon.args): - if vars(self.autorecon.args)[name] is None: - if default: - return default - else: - return None - else: - return vars(self.autorecon.args)[name] - else: - if default: - return default - return None - - @final - def get_global(self, name, default=None): - return self.get_global_option(name, default) - - @final - def add_pattern(self, pattern, description=None): - try: - compiled = re.compile(pattern) - if description: - self.patterns.append(Pattern(compiled, description=description)) - else: - self.patterns.append(Pattern(compiled)) - except re.error: - fail('Error: The pattern "' + pattern + '" in the plugin "' + self.name + '" is invalid regex.') - -class PortScan(Plugin): - - def __init__(self): - super().__init__() - self.type = None - - async def run(self, target): - raise NotImplementedError - -class ServiceScan(Plugin): - - def __init__(self): - super().__init__() - self.ports = {'tcp':[], 'udp':[]} - self.ignore_ports = {'tcp':[], 'udp':[]} - self.services = [] - self.service_names = [] - self.ignore_service_names = [] - self.match_all_service_names_boolean = False - self.run_once_boolean = False - self.require_ssl_boolean = False - - @final - def match_service(self, protocol, port, name, negative_match=False): - protocol = protocol.lower() - if protocol not in ['tcp', 'udp']: - print('Invalid protocol.') - sys.exit(1) - - if not isinstance(port, list): - port = [port] - - port = list(map(int, port)) - - if not isinstance(name, list): - name = [name] - - valid_regex = True - for r in name: - try: - re.compile(r) - except re.error: - print('Invalid regex: ' + r) - valid_regex = False - - if not valid_regex: - sys.exit(1) - - service = {'protocol': protocol, 'port': port, 'name': name, 'negative_match': negative_match} - self.services.append(service) - - @final - def match_port(self, protocol, port, negative_match=False): - protocol = protocol.lower() - if protocol not in ['tcp', 'udp']: - print('Invalid protocol.') - sys.exit(1) - else: - if not isinstance(port, list): - port = [port] - - port = list(map(int, port)) - - if negative_match: - self.ignore_ports[protocol] = list(set(self.ignore_ports[protocol] + port)) - else: - self.ports[protocol] = list(set(self.ports[protocol] + port)) - - @final - def match_service_name(self, name, negative_match=False): - if not isinstance(name, list): - name = [name] - - valid_regex = True - for r in name: - try: - re.compile(r) - except re.error: - print('Invalid regex: ' + r) - valid_regex = False - - if valid_regex: - if negative_match: - self.ignore_service_names = list(set(self.ignore_service_names + name)) - else: - self.service_names = list(set(self.service_names + name)) - else: - sys.exit(1) - - @final - def require_ssl(self, boolean): - self.require_ssl_boolean = boolean - - @final - def run_once(self, boolean): - self.run_once_boolean = boolean - - @final - def match_all_service_names(self, boolean): - self.match_all_service_names_boolean = boolean - -class AutoRecon(object): - - def __init__(self): - self.pending_targets = [] - self.scanning_targets = [] - self.plugins = {} - self.__slug_regex = re.compile('^[a-z0-9\-]+$') - self.plugin_types = {'port':[], 'service':[]} - self.port_scan_semaphore = None - self.service_scan_semaphore = None - self.argparse = None - self.argparse_group = None - self.args = None - self.missing_services = [] - self.taglist = [] - self.tags = [] - self.excluded_tags = [] - self.patterns = [] - self.configurable_keys = [ - 'ports', - 'max_scans', - 'max_port_scans', - 'tags', - 'exclude_tags', - 'plugins_dir', - 'add_plugins-dir', - 'outdir', - 'single_target', - 'only_scans_dir', - 'create_port_dirs', - 'heartbeat', - 'timeout', - 'target_timeout', - 'nmap', - 'nmap_append', - 'disable_sanity_checks', - 'disable_keyboard_control', - 'force_services', - 'accessible', - 'verbose' - ] - self.configurable_boolean_keys = ['single_target', 'only_scans_dir', 'create_port_dirs', 'disable_sanity_checks', 'accessible'] - self.config = { - 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], - 'global_file': os.path.dirname(os.path.realpath(__file__)) + '/global.toml', - 'ports': None, - 'max_scans': 50, - 'max_port_scans': None, - 'tags': 'default', - 'exclude_tags': None, - 'plugins_dir': os.path.dirname(os.path.abspath(__file__)) + '/plugins', - 'add_plugins_dir': None, - 'outdir': 'results', - 'single_target': False, - 'only_scans_dir': False, - 'create_port_dirs': False, - 'heartbeat': 60, - 'timeout': None, - 'target_timeout': None, - 'nmap': '-vv --reason -Pn', - 'nmap_append': '', - 'disable_sanity_checks': False, - 'disable_keyboard_control': False, - 'force_services': None, - 'accessible': False, - 'verbose': 0 - } - self.errors = False - self.lock = asyncio.Lock() - self.load_slug = None - self.load_module = None - - def add_argument(self, plugin, name, **kwargs): - # TODO: make sure name is simple. - name = '--' + plugin.slug + '.' + slugify(name) - - if self.argparse_group is None: - self.argparse_group = self.argparse.add_argument_group('plugin arguments', description='These are optional arguments for certain plugins.') - self.argparse_group.add_argument(name, **kwargs) - - def extract_service(self, line, regex): - if regex is None: - regex = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' - match = re.search(regex, line) - if match: - protocol = match.group('protocol').lower() - port = int(match.group('port')) - service = match.group('service') - secure = True if 'ssl' in service or 'tls' in service else False - - if service.startswith('ssl/') or service.startswith('tls/'): - service = service[4:] - - from autorecon import Service - return Service(protocol, port, service, secure) - else: - return None - - async def extract_services(self, stream, regex): - if not isinstance(stream, CommandStreamReader): - print('Error: extract_services must be passed an instance of a CommandStreamReader.') - sys.exit(1) - - services = [] - while True: - line = await stream.readline() - if line is not None: - service = self.extract_service(line, regex) - if service: - services.append(service) - else: - break - return services - - def register(self, plugin, filename): - if plugin.disabled: - return - - for _, loaded_plugin in self.plugins.items(): - if plugin.name == loaded_plugin.name: - fail('Error: Duplicate plugin name "' + plugin.name + '" detected in ' + filename + '.', file=sys.stderr) - - if plugin.slug is None: - plugin.slug = slugify(plugin.name) - elif not self.__slug_regex.match(plugin.slug): - fail('Error: provided slug "' + plugin.slug + '" in ' + filename + ' is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) - - if plugin.slug in self.config['protected_classes']: - fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is a protected string. Please change.') - - if plugin.slug not in self.plugins: - - for _, loaded_plugin in self.plugins.items(): - if plugin is loaded_plugin: - fail('Error: plugin "' + plugin.name + '" in ' + filename + ' already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) - - configure_function_found = False - run_coroutine_found = False - manual_function_found = False - - for member_name, member_value in inspect.getmembers(plugin, predicate=inspect.ismethod): - if member_name == 'configure': - configure_function_found = True - elif member_name == 'run' and inspect.iscoroutinefunction(member_value): - if len(inspect.getfullargspec(member_value).args) != 2: - fail('Error: the "run" coroutine in the plugin "' + plugin.name + '" in ' + filename + ' should have two arguments.', file=sys.stderr) - run_coroutine_found = True - elif member_name == 'manual': - if len(inspect.getfullargspec(member_value).args) != 3: - fail('Error: the "manual" function in the plugin "' + plugin.name + '" in ' + filename + ' should have three arguments.', file=sys.stderr) - manual_function_found = True - - if not run_coroutine_found and not manual_function_found: - fail('Error: the plugin "' + plugin.name + '" in ' + filename + ' needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) - - from autorecon import PortScan, ServiceScan - if issubclass(plugin.__class__, PortScan): - self.plugin_types["port"].append(plugin) - elif issubclass(plugin.__class__, ServiceScan): - self.plugin_types["service"].append(plugin) - else: - fail('Plugin "' + plugin.name + '" in ' + filename + ' is neither a PortScan nor a ServiceScan.', file=sys.stderr) - - plugin.tags = [tag.lower() for tag in plugin.tags] - - # Add plugin tags to tag list. - [autorecon.taglist.append(t) for t in plugin.tags if t not in autorecon.tags] - - plugin.autorecon = self - if configure_function_found: - plugin.configure() - self.plugins[plugin.slug] = plugin - else: - fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is already assigned.', file=sys.stderr) - - async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None): - if patterns: - combined_patterns = self.patterns + patterns - else: - combined_patterns = self.patterns - - process = await asyncio.create_subprocess_shell( - cmd, - stdin=open('/dev/null'), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE) - - cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile) - cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile) - - asyncio.create_task(cout._read()) - asyncio.create_task(cerr._read()) - - return process, cout, cerr - -# Since this file is run as the main method and also imported by plugins, -# we need to make sure that only one instance of the AutoRecon is -# created. This cannot be done with Singletons unfortunately, which is -# why this hack is here. -if 'autorecon' not in sys.modules: # If this file is not yet imported, create the AutoRecon object - autorecon = AutoRecon() -else: # Otherwise, assign it from the __main__ module. - autorecon = sys.modules['__main__'].autorecon - -def e(*args, frame_index=1, **kvargs): - frame = sys._getframe(frame_index) - - vals = {} - - vals.update(frame.f_globals) - vals.update(frame.f_locals) - vals.update(kvargs) - - return string.Formatter().vformat(' '.join(args), args, vals) - -def fformat(s): - return e(s, frame_index=3) - -def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, **kvargs): - frame = sys._getframe(frame_index) - - vals = { - 'bgreen': Fore.GREEN + Style.BRIGHT, - 'bred': Fore.RED + Style.BRIGHT, - 'bblue': Fore.BLUE + Style.BRIGHT, - 'byellow': Fore.YELLOW + Style.BRIGHT, - 'bmagenta': Fore.MAGENTA + Style.BRIGHT, - - 'green': Fore.GREEN, - 'red': Fore.RED, - 'blue': Fore.BLUE, - 'yellow': Fore.YELLOW, - 'magenta': Fore.MAGENTA, - - 'bright': Style.BRIGHT, - 'srst': Style.NORMAL, - 'crst': Fore.RESET, - 'rst': Style.NORMAL + Fore.RESET - } - - if autorecon.config['accessible']: - vals = {'bgreen':'', 'bred':'', 'bblue':'', 'byellow':'', 'bmagenta':'', 'green':'', 'red':'', 'blue':'', 'yellow':'', 'magenta':'', 'bright':'', 'srst':'', 'crst':'', 'rst':''} - - vals.update(frame.f_globals) - vals.update(frame.f_locals) - vals.update(kvargs) - - unfmt = '' - if char is not None and not autorecon.config['accessible']: - unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep - unfmt += sep.join(args) - - fmted = unfmt - - for attempt in range(10): - try: - fmted = string.Formatter().vformat(unfmt, args, vals) - break - except KeyError as err: - key = err.args[0] - unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') - - if printmsg: - print(fmted, sep=sep, end=end, file=file) - else: - return fmted - -def debug(*args, color=Fore.GREEN, sep=' ', end='\n', file=sys.stdout, **kvargs): - if verbose >= 2: - if autorecon.config['accessible']: - args = ('Debug:',) + args - cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def info(*args, sep=' ', end='\n', file=sys.stdout, **kvargs): - cprint(*args, color=Fore.BLUE, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def warn(*args, sep=' ', end='\n', file=sys.stderr,**kvargs): - if autorecon.config['accessible']: - args = ('Warning:',) + args - cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def error(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): - if autorecon.config['accessible']: - args = ('Error:',) + args - cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) - -def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): - if autorecon.config['accessible']: - args = ('Failure:',) + args - cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) - exit(-1) +autorecon = AutoRecon() def calculate_elapsed_time(start_time, short=False): elapsed_seconds = round(time.time() - start_time) @@ -771,9 +58,6 @@ def calculate_elapsed_time(start_time, short=False): else: return ', '.join(elapsed_time) -def slugify(name): - return re.sub(r'[\W_]+', '-', unidecode.unidecode(name).lower()).strip('-') - def cancel_all_tasks(signal, frame): for task in asyncio.all_tasks(): task.cancel() @@ -786,13 +70,10 @@ def cancel_all_tasks(signal, frame): except ProcessLookupError: # Will get raised if the process finishes before we get to killing it. pass - if not autorecon.config['disable_keyboard_control']: + if not config['disable_keyboard_control']: # Restore original terminal settings. termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, terminal_settings) -def timeout(signal, frame): - raise Exception("Function timed out.") - async def start_heartbeat(target, period=60): while True: await asyncio.sleep(period) @@ -800,7 +81,7 @@ async def start_heartbeat(target, period=60): count = len(target.running_tasks) tasks_list = '' - if target.autorecon.config['verbose'] >= 1: + if config['verbose'] >= 1: tasks_list = ': {bblue}' + ', '.join(target.running_tasks.keys()) + '{rst}' current_time = datetime.now().strftime('%H:%M:%S') @@ -819,18 +100,18 @@ async def keyboard(): if len(input) >= 3: if input[:3] == '\x1b[A': input = '' - if autorecon.config['verbose'] == 2: + if config['verbose'] == 2: info('Verbosity is already at the highest level.') else: - autorecon.config['verbose'] += 1 - info('Verbosity increased to ' + str(autorecon.config['verbose'])) + config['verbose'] += 1 + info('Verbosity increased to ' + str(config['verbose'])) elif input[:3] == '\x1b[B': input = '' - if autorecon.config['verbose'] == 0: + if config['verbose'] == 0: info('Verbosity is already at the lowest level.') else: - autorecon.config['verbose'] -= 1 - info('Verbosity decreased to ' + str(autorecon.config['verbose'])) + config['verbose'] -= 1 + info('Verbosity decreased to ' + str(config['verbose'])) else: if input[0] != 's': input = input[1:] @@ -841,7 +122,7 @@ async def keyboard(): count = len(target.running_tasks) tasks_list = [] - if target.autorecon.config['verbose'] >= 1: + if config['verbose'] >= 1: for key, value in target.running_tasks.items(): elapsed_time = calculate_elapsed_time(value['start'], short=True) tasks_list.append('{bblue}' + key + '{rst}' + ' (elapsed: ' + elapsed_time + ')') @@ -861,26 +142,26 @@ async def keyboard(): await asyncio.sleep(0.1) async def port_scan(plugin, target): - if autorecon.config['ports']: - if autorecon.config['ports']['tcp'] or autorecon.config['ports']['udp']: + if config['ports']: + if config['ports']['tcp'] or config['ports']['udp']: target.ports = {'tcp':None, 'udp':None} - if autorecon.config['ports']['tcp']: - target.ports['tcp'] = ','.join(autorecon.config['ports']['tcp']) - if autorecon.config['ports']['udp']: - target.ports['udp'] = ','.join(autorecon.config['ports']['udp']) + if config['ports']['tcp']: + target.ports['tcp'] = ','.join(config['ports']['tcp']) + if config['ports']['udp']: + target.ports['udp'] = ','.join(config['ports']['udp']) if plugin.type is None: - warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} does not have a type set, and --ports was used. Skipping.') + warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} does not have a type set, and --ports was used. Skipping.') return {'type':'port', 'plugin':plugin, 'result':[]} else: - if plugin.type == 'tcp' and not autorecon.config['ports']['tcp']: - warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping') + if plugin.type == 'tcp' and not config['ports']['tcp']: + warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping') return {'type':'port', 'plugin':plugin, 'result':[]} - elif plugin.type == 'udp' and not autorecon.config['ports']['udp']: - warn('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping') + elif plugin.type == 'udp' and not config['ports']['udp']: + warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping') return {'type':'port', 'plugin':plugin, 'result':[]} async with target.autorecon.port_scan_semaphore: - info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') + info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') start_time = time.time() @@ -892,11 +173,11 @@ async def port_scan(plugin, target): except Exception as ex: exc_type, exc_value, exc_tb = sys.exc_info() error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) - raise Exception(cprint('Error: Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) + raise Exception(cprint('Error: Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) for process_dict in target.running_tasks[plugin.slug]['processes']: if process_dict['process'].returncode is None: - warn('A process was left running after port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + warn('A process was left running after port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') await process_dict['process'].wait() if process_dict['process'].returncode != 0: @@ -907,7 +188,7 @@ async def port_scan(plugin, target): errors.append(line + '\n') else: break - error('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.') + error('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.') async with target.lock: with open(os.path.join(target.scandir, '_errors.log'), 'a') as file: file.writelines('[*] Port scan ' + plugin.name + ' (' + plugin.slug + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') @@ -922,14 +203,14 @@ async def port_scan(plugin, target): async with target.lock: target.running_tasks.pop(plugin.slug, None) - info('Port scan {bblue}' + plugin.name + ' (' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time) + info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time) return {'type':'port', 'plugin':plugin, 'result':result} async def service_scan(plugin, service): - from autorecon import PortScan + #from autorecon import PortScan semaphore = service.target.autorecon.service_scan_semaphore - if not service.target.autorecon.config['force_services']: + if not config['force_services']: # If service scan semaphore is locked, see if we can use port scan semaphore. while True: if semaphore.locked(): @@ -941,14 +222,14 @@ async def service_scan(plugin, service): if issubclass(process_list['plugin'].__class__, PortScan): port_scan_task_count += 1 - if not service.target.autorecon.pending_targets and (service.target.autorecon.config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. + if not service.target.autorecon.pending_targets and (config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. if service.target.autorecon.port_scan_semaphore.locked(): await asyncio.sleep(1) continue semaphore = service.target.autorecon.port_scan_semaphore break else: # Do some math to see if we can use the port scan semaphore. - if (service.target.autorecon.config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * service.target.autorecon.config['port_scan_plugin_count']))) >= 1: + if (config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * config['port_scan_plugin_count']))) >= 1: if service.target.autorecon.port_scan_semaphore.locked(): await asyncio.sleep(1) continue @@ -986,7 +267,7 @@ async def service_scan(plugin, service): tag = service.tag() + '/' + plugin.slug - info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') + info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') start_time = time.time() @@ -998,11 +279,11 @@ async def service_scan(plugin, service): except Exception as ex: exc_type, exc_value, exc_tb = sys.exc_info() error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) - raise Exception(cprint('Error: Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} running against {byellow}' + service.target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) + raise Exception(cprint('Error: Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} running against {byellow}' + service.target.address + '{rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) for process_dict in service.target.running_tasks[tag]['processes']: if process_dict['process'].returncode is None: - warn('A process was left running after service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + warn('A process was left running after service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') await process_dict['process'].wait() if process_dict['process'].returncode != 0: @@ -1013,7 +294,7 @@ async def service_scan(plugin, service): errors.append(line + '\n') else: break - error('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.') + error('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.') async with service.target.lock: with open(os.path.join(service.target.scandir, '_errors.log'), 'a') as file: file.writelines('[*] Service scan ' + plugin.name + ' (' + tag + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') @@ -1028,18 +309,21 @@ async def service_scan(plugin, service): async with service.target.lock: service.target.running_tasks.pop(tag, None) - info('Service scan {bblue}' + plugin.name + ' (' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time) + info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time) return {'type':'service', 'plugin':plugin, 'result':result} async def scan_target(target): - if target.autorecon.config['single_target']: - basedir = os.path.abspath(target.autorecon.config['outdir']) - else: - basedir = os.path.abspath(os.path.join(target.autorecon.config['outdir'], target.address)) - target.basedir = basedir - os.makedirs(basedir, exist_ok=True) + os.makedirs(os.path.abspath(config['outdir']), exist_ok=True) - if not target.autorecon.config['only_scans_dir']: + if config['single_target']: + basedir = os.path.abspath(config['outdir']) + else: + basedir = os.path.abspath(os.path.join(config['outdir'], target.address)) + os.makedirs(basedir, exist_ok=True) + + target.basedir = basedir + + if not config['only_scans_dir']: exploitdir = os.path.join(basedir, 'exploit') os.makedirs(exploitdir, exist_ok=True) @@ -1064,11 +348,11 @@ async def scan_target(target): pending = [] - heartbeat = asyncio.create_task(start_heartbeat(target, period=target.autorecon.config['heartbeat'])) + heartbeat = asyncio.create_task(start_heartbeat(target, period=config['heartbeat'])) services = [] - if autorecon.config['force_services']: - forced_services = [x.strip().lower() for x in autorecon.config['force_services']] + if config['force_services']: + forced_services = [x.strip().lower() for x in config['force_services']] for forced_service in forced_services: match = re.search('(?P(tcp|udp))\/(?P\d+)\/(?P[\w\-\/]+)\/(?Psecure|insecure)', forced_service) @@ -1119,14 +403,14 @@ async def scan_target(target): done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) # Check if global timeout has occurred. - if autorecon.config['target_timeout'] is not None: + if config['target_timeout'] is not None: elapsed_seconds = round(time.time() - start_time) m, s = divmod(elapsed_seconds, 60) - if m >= autorecon.config['target_timeout']: + if m >= config['target_timeout']: timed_out = True break - if not autorecon.config['force_services']: + if not config['force_services']: # Extract Services services = [] @@ -1154,7 +438,7 @@ async def scan_target(target): info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') - if not autorecon.config['only_scans_dir']: + if not config['only_scans_dir']: with open(os.path.join(target.reportdir, 'notes.txt'), 'a') as file: file.writelines('[*] ' + service.name + ' found on ' + service.protocol + '/' + str(service.port) + '.\n\n\n\n') @@ -1313,9 +597,12 @@ async def scan_target(target): for process_list in target.running_tasks.values(): for process_dict in process_list['processes']: - process_dict['process'].kill() + try: + process_dict['process'].kill() + except ProcessLookupError: + pass - warn('{byellow}Scanning target ' + target.address + ' took longer than the specified target period (' + str(autorecon.config['target_timeout']) + ' min). Cancelling scans and moving to next target.{rst}') + warn('{byellow}Scanning target ' + target.address + ' took longer than the specified target period (' + str(config['target_timeout']) + ' min). Cancelling scans and moving to next target.{rst}') else: info('Finished scanning target {byellow}' + target.address + '{rst} in ' + elapsed_time) @@ -1323,8 +610,6 @@ async def scan_target(target): autorecon.scanning_targets.remove(target) async def main(): - from autorecon import Plugin, PortScan, ServiceScan, Target # We have to do this to get around issubclass weirdness when loading plugins. - parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') @@ -1362,7 +647,7 @@ async def main(): autorecon.argparse = parser if args.version: - print('AutoRecon v2.0-beta2') + print('AutoRecon v2.0-beta3') sys.exit(0) # Parse config file and args for global.toml first. @@ -1375,11 +660,11 @@ async def main(): for key, val in config_toml.items(): key = slugify(key) if key == 'global-file': - autorecon.config['global_file'] = val + config['global_file'] = val elif key == 'plugins-dir': - autorecon.config['plugins_dir'] = val + config['plugins_dir'] = val elif key == 'add-plugins-dir': - autorecon.config['add_plugins_dir'] = val + config['add_plugins_dir'] = val except toml.decoder.TomlDecodeError: fail('Error: Couldn\'t parse ' + args.config_file + ' config file. Check syntax.') @@ -1387,21 +672,21 @@ async def main(): for key in args_dict: key = slugify(key) if key == 'global-file' and args_dict['global_file'] is not None: - autorecon.config['global_file'] = args_dict['global_file'] + config['global_file'] = args_dict['global_file'] elif key == 'plugins-dir' and args_dict['plugins_dir'] is not None: - autorecon.config['plugins_dir'] = args_dict['plugins_dir'] + config['plugins_dir'] = args_dict['plugins_dir'] elif key == 'add-plugins-dir' and args_dict['add_plugins_dir'] is not None: - autorecon.config['add_plugins_dir'] = args_dict['add_plugins_dir'] + config['add_plugins_dir'] = args_dict['add_plugins_dir'] - if not os.path.isdir(autorecon.config['plugins_dir']): - fail('Error: Specified plugins directory "' + autorecon.config['plugins_dir'] + '" does not exist.') + if not os.path.isdir(config['plugins_dir']): + fail('Error: Specified plugins directory "' + config['plugins_dir'] + '" does not exist.') - if autorecon.config['add_plugins_dir'] and not os.path.isdir(autorecon.config['add_plugins_dir']): - fail('Error: Specified additional plugins directory "' + autorecon.config['add_plugins_dir'] + '" does not exist.') + if config['add_plugins_dir'] and not os.path.isdir(config['add_plugins_dir']): + fail('Error: Specified additional plugins directory "' + config['add_plugins_dir'] + '" does not exist.') - plugins_dirs = [autorecon.config['plugins_dir']] - if autorecon.config['add_plugins_dir']: - plugins_dirs.append(autorecon.config['add_plugins_dir']) + plugins_dirs = [config['plugins_dir']] + if config['add_plugins_dir']: + plugins_dirs.append(config['add_plugins_dir']) for plugins_dir in plugins_dirs: for plugin_file in os.listdir(plugins_dir): @@ -1419,10 +704,10 @@ async def main(): sys.path.pop(1) clsmembers = inspect.getmembers(plugin, predicate=inspect.isclass) for (_, c) in clsmembers: - if c.__module__ == 'autorecon': + if c.__module__ in ['autorecon.plugins', 'autorecon.targets']: continue - if c.__name__.lower() in autorecon.config['protected_classes']: + if c.__name__.lower() in config['protected_classes']: print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.') sys.exit(1) @@ -1443,17 +728,17 @@ async def main(): plugin.tags += [plugin.slug] if len(autorecon.plugin_types['port']) == 0: - fail('Error: There are no valid PortScan plugins in the plugins directory "' + autorecon.config['plugins_dir'] + '".') + fail('Error: There are no valid PortScan plugins in the plugins directory "' + config['plugins_dir'] + '".') # Sort plugins by priority. autorecon.plugin_types['port'].sort(key=lambda x: x.priority) autorecon.plugin_types['service'].sort(key=lambda x: x.priority) - if not os.path.isfile(autorecon.config['global_file']): - fail('Error: Specified global file "' + autorecon.config['global_file'] + '" does not exist.') + if not os.path.isfile(config['global_file']): + fail('Error: Specified global file "' + config['global_file'] + '" does not exist.') global_plugin_args = None - with open(autorecon.config['global_file']) as g: + with open(config['global_file']) as g: try: global_toml = toml.load(g) for key, val in global_toml.items(): @@ -1538,12 +823,12 @@ async def main(): autorecon.argparse.set_defaults(**{slugify(key).replace('-', '_') + '.' + slugify(pkey).replace('-', '_'): pval}) else: # Process potential other options. key = key.replace('-', '_') - if key in autorecon.configurable_keys: + if key in configurable_keys: other_options.append(key) - autorecon.config[key] = val + config[key] = val autorecon.argparse.set_defaults(**{key: val}) - for key, val in autorecon.config.items(): + for key, val in config.items(): if key not in other_options: autorecon.argparse.set_defaults(**{key: val}) @@ -1552,11 +837,11 @@ async def main(): args_dict = vars(args) for key in args_dict: - if key in autorecon.configurable_keys and args_dict[key] is not None: + if key in configurable_keys and args_dict[key] is not None: # Special case for booleans - if key in autorecon.configurable_boolean_keys and autorecon.config[key]: + if key in configurable_boolean_keys and config[key]: continue - autorecon.config[key] = args_dict[key] + config[key] = args_dict[key] autorecon.args = args if args.list: @@ -1570,11 +855,11 @@ async def main(): sys.exit(0) - if autorecon.config['ports']: + if config['ports']: ports_to_scan = {'tcp':[], 'udp':[]} unique = {'tcp':[], 'udp':[]} - ports = autorecon.config['ports'].split(',') + ports = config['ports'].split(',') mode = 'both' for port in ports: port = port.strip() @@ -1643,49 +928,49 @@ async def main(): unique['udp'].append(num) else: fail('Error: Invalid port number: ' + str(port)) - autorecon.config['ports'] = ports_to_scan + config['ports'] = ports_to_scan - if autorecon.config['max_scans'] <= 0: + if config['max_scans'] <= 0: error('Argument -m/--max-scans must be at least 1.') errors = True - if autorecon.config['max_port_scans'] is None: - autorecon.config['max_port_scans'] = max(1, round(autorecon.config['max_scans'] * 0.2)) + if config['max_port_scans'] is None: + config['max_port_scans'] = max(1, round(config['max_scans'] * 0.2)) else: - if autorecon.config['max_port_scans'] <= 0: + if config['max_port_scans'] <= 0: error('Argument -mp/--max-port-scans must be at least 1.') errors = True - if autorecon.config['max_port_scans'] > autorecon.config['max_scans']: + if config['max_port_scans'] > config['max_scans']: error('Argument -mp/--max-port-scans cannot be greater than argument -m/--max-scans.') errors = True - if autorecon.config['heartbeat'] <= 0: + if config['heartbeat'] <= 0: error('Argument --heartbeat must be at least 1.') errors = True - if autorecon.config['timeout'] is not None and autorecon.config['timeout'] <= 0: + if config['timeout'] is not None and config['timeout'] <= 0: error('Argument --timeout must be at least 1.') errors = True - if autorecon.config['target_timeout'] is not None and autorecon.config['target_timeout'] <= 0: + if config['target_timeout'] is not None and config['target_timeout'] <= 0: error('Argument --target-timeout must be at least 1.') errors = True - if autorecon.config['timeout'] is not None and autorecon.config['target_timeout'] is not None and autorecon.config['timeout'] < autorecon.config['target_timeout']: + if config['timeout'] is not None and config['target_timeout'] is not None and config['timeout'] < config['target_timeout']: error('Argument --timeout cannot be less than --target-timeout.') errors = True if not errors: - if autorecon.config['force_services']: - autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans']) + if config['force_services']: + autorecon.service_scan_semaphore = asyncio.Semaphore(config['max_scans']) else: - autorecon.port_scan_semaphore = asyncio.Semaphore(autorecon.config['max_port_scans']) + autorecon.port_scan_semaphore = asyncio.Semaphore(config['max_port_scans']) # If max scans and max port scans is the same, the service scan semaphore and port scan semaphore should be the same object - if autorecon.config['max_scans'] == autorecon.config['max_port_scans']: + if config['max_scans'] == config['max_port_scans']: autorecon.service_scan_semaphore = autorecon.port_scan_semaphore else: - autorecon.service_scan_semaphore = asyncio.Semaphore(autorecon.config['max_scans'] - autorecon.config['max_port_scans']) + autorecon.service_scan_semaphore = asyncio.Semaphore(config['max_scans'] - config['max_port_scans']) tags = [] for tag_group in list(set(filter(None, args.tags.lower().split(',')))): @@ -1751,7 +1036,7 @@ async def main(): try: target_range = ipaddress.ip_network(target, strict=False) if not args.disable_sanity_checks and target_range.num_addresses > 256: - error(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') + fail(target + ' contains ' + str(target_range.num_addresses) + ' addresses. Check that your CIDR notation is correct. If it is, re-run with the --disable-sanity-checks option to suppress this check.') errors = True else: for ip in target_range.hosts(): @@ -1810,7 +1095,7 @@ async def main(): error('You must specify at least one target to scan!') errors = True - if autorecon.config['single_target'] and len(autorecon.pending_targets) != 1: + if config['single_target'] and len(autorecon.pending_targets) != 1: error('You cannot provide more than one target when scanning in single-target mode.') errors = True @@ -1818,7 +1103,7 @@ async def main(): error('A total of ' + str(len(autorecon.pending_targets)) + ' targets would be scanned. If this is correct, re-run with the --disable-sanity-checks option to suppress this check.') errors = True - if not autorecon.config['force_services']: + if not config['force_services']: port_scan_plugin_count = 0 for plugin in autorecon.plugin_types['port']: matching_tags = False @@ -1840,14 +1125,14 @@ async def main(): error('There are no port scan plugins that match the tags specified.') errors = True else: - port_scan_plugin_count = autorecon.config['max_port_scans'] / 5 + port_scan_plugin_count = config['max_port_scans'] / 5 if errors: sys.exit(1) - autorecon.config['port_scan_plugin_count'] = port_scan_plugin_count + config['port_scan_plugin_count'] = port_scan_plugin_count - num_initial_targets = max(1, math.ceil(autorecon.config['max_port_scans'] / port_scan_plugin_count)) + num_initial_targets = max(1, math.ceil(config['max_port_scans'] / port_scan_plugin_count)) start_time = time.time() @@ -1859,7 +1144,7 @@ async def main(): if i >= num_initial_targets: break - if not autorecon.config['disable_keyboard_control']: + if not config['disable_keyboard_control']: tty.setcbreak(sys.stdin.fileno()) keyboard_monitor = asyncio.create_task(keyboard()) @@ -1873,10 +1158,10 @@ async def main(): sys.exit(1) # Check if global timeout has occurred. - if autorecon.config['timeout'] is not None: + if config['timeout'] is not None: elapsed_seconds = round(time.time() - start_time) m, s = divmod(elapsed_seconds, 60) - if m >= autorecon.config['timeout']: + if m >= config['timeout']: timed_out = True break @@ -1890,14 +1175,14 @@ async def main(): for targ in autorecon.scanning_targets: for process_list in targ.running_tasks.values(): # If we're not scanning ports, count ServiceScans instead. - if autorecon.config['force_services']: + if config['force_services']: if issubclass(process_list['plugin'].__class__, ServiceScan): # TODO should we really count ServiceScans? Test... port_scan_task_count += 1 else: if issubclass(process_list['plugin'].__class__, PortScan): port_scan_task_count += 1 - num_new_targets = math.ceil((autorecon.config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count) + num_new_targets = math.ceil((config['max_port_scans'] - port_scan_task_count) / port_scan_plugin_count) if num_new_targets > 0: i = 0 while autorecon.pending_targets: @@ -1912,7 +1197,7 @@ async def main(): cancel_all_tasks(None, None) elapsed_time = calculate_elapsed_time(start_time) - warn('{byellow}AutoRecon took longer than the specified timeout period (' + str(autorecon.config['timeout']) + ' min). Cancelling all scans and exiting.{rst}') + warn('{byellow}AutoRecon took longer than the specified timeout period (' + str(config['timeout']) + ' min). Cancelling all scans and exiting.{rst}') else: while len(asyncio.all_tasks()) > 1: # this code runs in the main() task so it will be the only task left running await asyncio.sleep(1) @@ -1924,7 +1209,7 @@ async def main(): if autorecon.missing_services: warn('{byellow}AutoRecon identified the following services, but could not match them to any plugins based on the service name. Please report these to Tib3rius: ' + ', '.join(autorecon.missing_services) + '{rst}') - if not autorecon.config['disable_keyboard_control']: + if not config['disable_keyboard_control']: # Restore original terminal settings. termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) @@ -1935,3 +1220,5 @@ if __name__ == '__main__': asyncio.run(main()) except asyncio.exceptions.CancelledError: pass + except RuntimeError: + pass diff --git a/autorecon/__init__.py b/autorecon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autorecon/config.py b/autorecon/config.py new file mode 100644 index 0000000..5352c39 --- /dev/null +++ b/autorecon/config.py @@ -0,0 +1,59 @@ +import os + +configurable_keys = [ + 'ports', + 'max_scans', + 'max_port_scans', + 'tags', + 'exclude_tags', + 'plugins_dir', + 'add_plugins-dir', + 'outdir', + 'single_target', + 'only_scans_dir', + 'create_port_dirs', + 'heartbeat', + 'timeout', + 'target_timeout', + 'nmap', + 'nmap_append', + 'disable_sanity_checks', + 'disable_keyboard_control', + 'force_services', + 'accessible', + 'verbose' +] + +configurable_boolean_keys = [ + 'single_target', + 'only_scans_dir', + 'create_port_dirs', + 'disable_sanity_checks', + 'accessible' +] + +config = { + 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], + 'global_file': os.path.dirname(os.path.realpath(os.path.join(__file__, '..'))) + '/global.toml', + 'ports': None, + 'max_scans': 50, + 'max_port_scans': None, + 'tags': 'default', + 'exclude_tags': None, + 'plugins_dir': os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))) + '/plugins', + 'add_plugins_dir': None, + 'outdir': 'results', + 'single_target': False, + 'only_scans_dir': False, + 'create_port_dirs': False, + 'heartbeat': 60, + 'timeout': None, + 'target_timeout': None, + 'nmap': '-vv --reason -Pn', + 'nmap_append': '', + 'disable_sanity_checks': False, + 'disable_keyboard_control': False, + 'force_services': None, + 'accessible': False, + 'verbose': 0 +} diff --git a/autorecon/io.py b/autorecon/io.py new file mode 100644 index 0000000..ec15f0e --- /dev/null +++ b/autorecon/io.py @@ -0,0 +1,163 @@ +import asyncio, colorama, os, re, string, sys, unidecode +from colorama import Fore, Style +from autorecon.config import config + +def slugify(name): + return re.sub(r'[\W_]+', '-', unidecode.unidecode(name).lower()).strip('-') + +def e(*args, frame_index=1, **kvargs): + frame = sys._getframe(frame_index) + + vals = {} + + vals.update(frame.f_globals) + vals.update(frame.f_locals) + vals.update(kvargs) + + return string.Formatter().vformat(' '.join(args), args, vals) + +def fformat(s): + return e(s, frame_index=3) + +def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, **kvargs): + frame = sys._getframe(frame_index) + + vals = { + 'bgreen': Fore.GREEN + Style.BRIGHT, + 'bred': Fore.RED + Style.BRIGHT, + 'bblue': Fore.BLUE + Style.BRIGHT, + 'byellow': Fore.YELLOW + Style.BRIGHT, + 'bmagenta': Fore.MAGENTA + Style.BRIGHT, + + 'green': Fore.GREEN, + 'red': Fore.RED, + 'blue': Fore.BLUE, + 'yellow': Fore.YELLOW, + 'magenta': Fore.MAGENTA, + + 'bright': Style.BRIGHT, + 'srst': Style.NORMAL, + 'crst': Fore.RESET, + 'rst': Style.NORMAL + Fore.RESET + } + + if config['accessible']: + vals = {'bgreen':'', 'bred':'', 'bblue':'', 'byellow':'', 'bmagenta':'', 'green':'', 'red':'', 'blue':'', 'yellow':'', 'magenta':'', 'bright':'', 'srst':'', 'crst':'', 'rst':''} + + vals.update(frame.f_globals) + vals.update(frame.f_locals) + vals.update(kvargs) + + unfmt = '' + if char is not None and not config['accessible']: + unfmt += color + '[' + Style.BRIGHT + char + Style.NORMAL + ']' + Fore.RESET + sep + unfmt += sep.join(args) + + fmted = unfmt + + for attempt in range(10): + try: + fmted = string.Formatter().vformat(unfmt, args, vals) + break + except KeyError as err: + key = err.args[0] + unfmt = unfmt.replace('{' + key + '}', '{{' + key + '}}') + + if printmsg: + print(fmted, sep=sep, end=end, file=file) + else: + return fmted + +def debug(*args, color=Fore.GREEN, sep=' ', end='\n', file=sys.stdout, **kvargs): + if verbose >= 2: + if config['accessible']: + args = ('Debug:',) + args + cprint(*args, color=color, char='-', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def info(*args, sep=' ', end='\n', file=sys.stdout, **kvargs): + cprint(*args, color=Fore.BLUE, char='*', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def warn(*args, sep=' ', end='\n', file=sys.stderr,**kvargs): + if config['accessible']: + args = ('Warning:',) + args + cprint(*args, color=Fore.YELLOW, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def error(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): + if config['accessible']: + args = ('Error:',) + args + cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + +def fail(*args, sep=' ', end='\n', file=sys.stderr, **kvargs): + if config['accessible']: + args = ('Failure:',) + args + cprint(*args, color=Fore.RED, char='!', sep=sep, end=end, file=file, frame_index=2, **kvargs) + exit(-1) + +class CommandStreamReader(object): + + def __init__(self, stream, target, tag, patterns=None, outfile=None): + self.stream = stream + self.target = target + self.tag = tag + self.lines = [] + self.patterns = patterns or [] + self.outfile = outfile + self.ended = False + + # Read lines from the stream until it ends. + async def _read(self): + while True: + if self.stream.at_eof(): + break + try: + line = (await self.stream.readline()).decode('utf8').rstrip() + except ValueError: + error('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} A line was longer than 64 KiB and cannot be processed. Ignoring.') + continue + + if config['verbose'] >= 2: + if line != '': + info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} ' + line.replace('{', '{{').replace('}', '}}')) + + # Check lines for pattern matches. + for p in self.patterns: + matches = p.pattern.findall(line) + for match in matches: + async with self.target.lock: + with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file: + if p.description: + if config['verbose'] >= 1: + info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} {bmagenta}' + p.description.replace('{match}', match) + '{rst}') + file.writelines(p.description.replace('{match}', match) + '\n\n') + else: + if config['verbose'] >= 1: + info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} {bmagenta}Matched Pattern: ' + match + '{rst}') + file.writelines('Matched Pattern: ' + match + '\n\n') + + if self.outfile is not None: + with open(self.outfile, 'a') as writer: + writer.write(line + '\n') + self.lines.append(line) + self.ended = True + + # Read a line from the stream cache. + async def readline(self): + while True: + try: + return self.lines.pop(0) + except IndexError: + if self.ended: + return None + else: + await asyncio.sleep(0.1) + + # Read all lines from the stream cache. + async def readlines(self): + lines = [] + while True: + line = await self.readline() + if line is not None: + lines.append(line) + else: + break + return lines diff --git a/autorecon/plugins.py b/autorecon/plugins.py new file mode 100644 index 0000000..666bb94 --- /dev/null +++ b/autorecon/plugins.py @@ -0,0 +1,337 @@ +import asyncio, inspect, os, re, sys +from typing import final +from autorecon.config import config +from autorecon.io import slugify, error, fail, CommandStreamReader +from autorecon.targets import Service + +class Pattern: + + def __init__(self, pattern, description=None): + self.pattern = pattern + self.description = description + +class Plugin(object): + + def __init__(self): + self.name = None + self.slug = None + self.description = None + self.tags = ['default'] + self.priority = 1 + self.patterns = [] + self.autorecon = None + self.disabled = False + + @final + def add_option(self, name, default=None, help=None): + self.autorecon.add_argument(self, name, metavar='VALUE', default=default, help=help) + + @final + def add_constant_option(self, name, const, default=None, help=None): + self.autorecon.add_argument(self, name, action='store_const', const=const, default=default, help=help) + + @final + def add_true_option(self, name, help=None): + self.autorecon.add_argument(self, name, action='store_true', help=help) + + @final + def add_false_option(self, name, help=None): + self.autorecon.add_argument(self, name, action='store_false', help=help) + + @final + def add_list_option(self, name, default=None, help=None): + self.autorecon.add_argument(self, name, nargs='+', metavar='VALUE', default=default, help=help) + + @final + def add_choice_option(self, name, choices, default=None, help=None): + if not isinstance(choices, list): + fail('The choices argument for ' + self.name + '\'s ' + name + ' choice option should be a list.') + self.autorecon.add_argument(self, name, choices=choices, default=default, help=help) + + @final + def get_option(self, name): + # TODO: make sure name is simple. + name = self.slug.replace('-', '_') + '.' + slugify(name).replace('-', '_') + + if name in vars(self.autorecon.args): + return vars(self.autorecon.args)[name] + else: + return None + + @final + def get_global_option(self, name, default=None): + name = 'global.' + slugify(name).replace('-', '_') + + if name in vars(self.autorecon.args): + if vars(self.autorecon.args)[name] is None: + if default: + return default + else: + return None + else: + return vars(self.autorecon.args)[name] + else: + if default: + return default + return None + + @final + def get_global(self, name, default=None): + return self.get_global_option(name, default) + + @final + def add_pattern(self, pattern, description=None): + try: + compiled = re.compile(pattern) + if description: + self.patterns.append(Pattern(compiled, description=description)) + else: + self.patterns.append(Pattern(compiled)) + except re.error: + fail('Error: The pattern "' + pattern + '" in the plugin "' + self.name + '" is invalid regex.') + +class PortScan(Plugin): + + def __init__(self): + super().__init__() + self.type = None + + async def run(self, target): + raise NotImplementedError + +class ServiceScan(Plugin): + + def __init__(self): + super().__init__() + self.ports = {'tcp':[], 'udp':[]} + self.ignore_ports = {'tcp':[], 'udp':[]} + self.services = [] + self.service_names = [] + self.ignore_service_names = [] + self.match_all_service_names_boolean = False + self.run_once_boolean = False + self.require_ssl_boolean = False + + @final + def match_service(self, protocol, port, name, negative_match=False): + protocol = protocol.lower() + if protocol not in ['tcp', 'udp']: + print('Invalid protocol.') + sys.exit(1) + + if not isinstance(port, list): + port = [port] + + port = list(map(int, port)) + + if not isinstance(name, list): + name = [name] + + valid_regex = True + for r in name: + try: + re.compile(r) + except re.error: + print('Invalid regex: ' + r) + valid_regex = False + + if not valid_regex: + sys.exit(1) + + service = {'protocol': protocol, 'port': port, 'name': name, 'negative_match': negative_match} + self.services.append(service) + + @final + def match_port(self, protocol, port, negative_match=False): + protocol = protocol.lower() + if protocol not in ['tcp', 'udp']: + print('Invalid protocol.') + sys.exit(1) + else: + if not isinstance(port, list): + port = [port] + + port = list(map(int, port)) + + if negative_match: + self.ignore_ports[protocol] = list(set(self.ignore_ports[protocol] + port)) + else: + self.ports[protocol] = list(set(self.ports[protocol] + port)) + + @final + def match_service_name(self, name, negative_match=False): + if not isinstance(name, list): + name = [name] + + valid_regex = True + for r in name: + try: + re.compile(r) + except re.error: + print('Invalid regex: ' + r) + valid_regex = False + + if valid_regex: + if negative_match: + self.ignore_service_names = list(set(self.ignore_service_names + name)) + else: + self.service_names = list(set(self.service_names + name)) + else: + sys.exit(1) + + @final + def require_ssl(self, boolean): + self.require_ssl_boolean = boolean + + @final + def run_once(self, boolean): + self.run_once_boolean = boolean + + @final + def match_all_service_names(self, boolean): + self.match_all_service_names_boolean = boolean + +class AutoRecon(object): + + def __init__(self): + self.pending_targets = [] + self.scanning_targets = [] + self.plugins = {} + self.__slug_regex = re.compile('^[a-z0-9\-]+$') + self.plugin_types = {'port':[], 'service':[]} + self.port_scan_semaphore = None + self.service_scan_semaphore = None + self.argparse = None + self.argparse_group = None + self.args = None + self.missing_services = [] + self.taglist = [] + self.tags = [] + self.excluded_tags = [] + self.patterns = [] + self.errors = False + self.lock = asyncio.Lock() + self.load_slug = None + self.load_module = None + + def add_argument(self, plugin, name, **kwargs): + # TODO: make sure name is simple. + name = '--' + plugin.slug + '.' + slugify(name) + + if self.argparse_group is None: + self.argparse_group = self.argparse.add_argument_group('plugin arguments', description='These are optional arguments for certain plugins.') + self.argparse_group.add_argument(name, **kwargs) + + def extract_service(self, line, regex): + if regex is None: + regex = '^(?P\d+)\/(?P(tcp|udp))(.*)open(\s*)(?P[\w\-\/]+)(\s*)(.*)$' + match = re.search(regex, line) + if match: + protocol = match.group('protocol').lower() + port = int(match.group('port')) + service = match.group('service') + secure = True if 'ssl' in service or 'tls' in service else False + + if service.startswith('ssl/') or service.startswith('tls/'): + service = service[4:] + + return Service(protocol, port, service, secure) + else: + return None + + async def extract_services(self, stream, regex): + if not isinstance(stream, CommandStreamReader): + print('Error: extract_services must be passed an instance of a CommandStreamReader.') + sys.exit(1) + + services = [] + while True: + line = await stream.readline() + if line is not None: + service = self.extract_service(line, regex) + if service: + services.append(service) + else: + break + return services + + def register(self, plugin, filename): + if plugin.disabled: + return + + for _, loaded_plugin in self.plugins.items(): + if plugin.name == loaded_plugin.name: + fail('Error: Duplicate plugin name "' + plugin.name + '" detected in ' + filename + '.', file=sys.stderr) + + if plugin.slug is None: + plugin.slug = slugify(plugin.name) + elif not self.__slug_regex.match(plugin.slug): + fail('Error: provided slug "' + plugin.slug + '" in ' + filename + ' is not valid (must only contain lowercase letters, numbers, and hyphens).', file=sys.stderr) + + if plugin.slug in config['protected_classes']: + fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is a protected string. Please change.') + + if plugin.slug not in self.plugins: + + for _, loaded_plugin in self.plugins.items(): + if plugin is loaded_plugin: + fail('Error: plugin "' + plugin.name + '" in ' + filename + ' already loaded as "' + loaded_plugin.name + '" (' + str(loaded_plugin) + ')', file=sys.stderr) + + configure_function_found = False + run_coroutine_found = False + manual_function_found = False + + for member_name, member_value in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'configure': + configure_function_found = True + elif member_name == 'run' and inspect.iscoroutinefunction(member_value): + if len(inspect.getfullargspec(member_value).args) != 2: + fail('Error: the "run" coroutine in the plugin "' + plugin.name + '" in ' + filename + ' should have two arguments.', file=sys.stderr) + run_coroutine_found = True + elif member_name == 'manual': + if len(inspect.getfullargspec(member_value).args) != 3: + fail('Error: the "manual" function in the plugin "' + plugin.name + '" in ' + filename + ' should have three arguments.', file=sys.stderr) + manual_function_found = True + + if not run_coroutine_found and not manual_function_found: + fail('Error: the plugin "' + plugin.name + '" in ' + filename + ' needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) + + #from autorecon import PortScan, ServiceScan + if issubclass(plugin.__class__, PortScan): + self.plugin_types["port"].append(plugin) + elif issubclass(plugin.__class__, ServiceScan): + self.plugin_types["service"].append(plugin) + else: + fail('Plugin "' + plugin.name + '" in ' + filename + ' is neither a PortScan nor a ServiceScan.', file=sys.stderr) + + plugin.tags = [tag.lower() for tag in plugin.tags] + + # Add plugin tags to tag list. + [self.taglist.append(t) for t in plugin.tags if t not in self.tags] + + plugin.autorecon = self + if configure_function_found: + plugin.configure() + self.plugins[plugin.slug] = plugin + else: + fail('Error: plugin slug "' + plugin.slug + '" in ' + filename + ' is already assigned.', file=sys.stderr) + + async def execute(self, cmd, target, tag, patterns=None, outfile=None, errfile=None): + if patterns: + combined_patterns = self.patterns + patterns + else: + combined_patterns = self.patterns + + process = await asyncio.create_subprocess_shell( + cmd, + stdin=open('/dev/null'), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + + cout = CommandStreamReader(process.stdout, target, tag, patterns=combined_patterns, outfile=outfile) + cerr = CommandStreamReader(process.stderr, target, tag, patterns=combined_patterns, outfile=errfile) + + asyncio.create_task(cout._read()) + asyncio.create_task(cerr._read()) + + return process, cout, cerr diff --git a/autorecon/targets.py b/autorecon/targets.py new file mode 100644 index 0000000..49acc75 --- /dev/null +++ b/autorecon/targets.py @@ -0,0 +1,172 @@ +import asyncio, inspect, os +from typing import final +from autorecon.config import config +from autorecon.io import e, info + +class Target: + + def __init__(self, address, ipversion, type, autorecon): + self.address = address + self.ipversion = ipversion + self.type = type + self.autorecon = autorecon + self.basedir = '' + self.reportdir = '' + self.scandir = '' + self.lock = asyncio.Lock() + self.ports = None + self.pending_services = [] + self.services = [] + self.scans = [] + self.running_tasks = {} + + async def add_service(self, service): + async with self.lock: + self.pending_services.append(service) + + def extract_service(self, line, regex=None): + return self.autorecon.extract_service(line, regex) + + async def extract_services(self, stream, regex=None): + return await self.autorecon.extract_services(stream, regex) + + async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + target = self + + # Create variables for command references. + address = target.address + addressv6 = target.address + scandir = target.scandir + + nmap_extra = target.autorecon.args.nmap + if target.autorecon.args.nmap_append: + nmap_extra += ' ' + target.autorecon.args.nmap_append + + if target.ipversion == 'IPv6': + nmap_extra += ' -6' + addressv6 = '[' + addressv6 + ']' + + plugin = inspect.currentframe().f_back.f_locals['self'] + + cmd = e(cmd) + + tag = plugin.slug + + if config['verbose'] >= 1: + info('Port scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + + if outfile is not None: + outfile = os.path.join(target.scandir, e(outfile)) + + if errfile is not None: + errfile = os.path.join(target.scandir, e(errfile)) + + async with target.lock: + with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: + file.writelines(cmd + '\n\n') + + process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) + + target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) + + # If process should block, sleep until stdout and stderr have finished. + if blocking: + while (not (stdout.ended and stderr.ended)): + await asyncio.sleep(0.1) + await process.wait() + + return process, stdout, stderr + +class Service: + + def __init__(self, protocol, port, name, secure=False): + self.target = None + self.protocol = protocol.lower() + self.port = int(port) + self.name = name + self.secure = secure + self.manual_commands = {} + + @final + def tag(self): + return self.protocol + '/' + str(self.port) + '/' + self.name + + @final + def full_tag(self): + return self.protocol + '/' + str(self.port) + '/' + self.name + '/' + ('secure' if self.secure else 'insecure') + + @final + def add_manual_commands(self, description, commands): + if not isinstance(commands, list): + commands = [commands] + if description not in self.manual_commands: + self.manual_commands[description] = [] + + # Merge in new unique commands, while preserving order. + [self.manual_commands[description].append(m) for m in commands if m not in self.manual_commands[description]] + + @final + def add_manual_command(self, description, command): + self.add_manual_commands(description, command) + + @final + async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + target = self.target + + # Create variables for command references. + address = target.address + addressv6 = target.address + scandir = target.scandir + protocol = self.protocol + port = self.port + name = self.name + + if config['create_port_dirs']: + scandir = os.path.join(scandir, protocol + str(port)) + os.makedirs(scandir, exist_ok=True) + os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) + + # Special cases for HTTP. + http_scheme = 'https' if 'https' in self.name or self.secure is True else 'http' + + nmap_extra = target.autorecon.args.nmap + if target.autorecon.args.nmap_append: + nmap_extra += ' ' + target.autorecon.args.nmap_append + + if protocol == 'udp': + nmap_extra += ' -sU' + + if target.ipversion == 'IPv6': + nmap_extra += ' -6' + addressv6 = '[' + addressv6 + ']' + + plugin = inspect.currentframe().f_back.f_locals['self'] + + cmd = e(cmd) + + tag = self.tag() + '/' + plugin.slug + + if config['verbose'] >= 1: + info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + + if outfile is not None: + outfile = os.path.join(scandir, e(outfile)) + + if errfile is not None: + errfile = os.path.join(scandir, e(errfile)) + + async with target.lock: + with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: + file.writelines(cmd + '\n\n') + + process, stdout, stderr = await target.autorecon.execute(cmd, target, tag, patterns=plugin.patterns, outfile=outfile, errfile=errfile) + + target.running_tasks[tag]['processes'].append({'process': process, 'stderr': stderr, 'cmd': cmd}) + + # If process should block, sleep until stdout and stderr have finished. + if blocking: + while (not (stdout.ended and stderr.ended)): + await asyncio.sleep(0.1) + await process.wait() + + return process, stdout, stderr diff --git a/plugins/databases.py b/plugins/databases.py index e135de4..dab52a6 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapMongoDB(ServiceScan): diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index 6bbaa51..0246d3a 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -1,4 +1,5 @@ -from autorecon import PortScan, error +from autorecon.plugins import PortScan +from autorecon.io import error import os class QuickTCPPortScan(PortScan): diff --git a/plugins/dns.py b/plugins/dns.py index 1f8cfe0..8793b3b 100644 --- a/plugins/dns.py +++ b/plugins/dns.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapDNS(ServiceScan): diff --git a/plugins/ftp.py b/plugins/ftp.py index cb9b7ca..edd470b 100644 --- a/plugins/ftp.py +++ b/plugins/ftp.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapFTP(ServiceScan): diff --git a/plugins/guess-port-scan.py b/plugins/guess-port-scan.py index 89ad72a..6ae5fa9 100644 --- a/plugins/guess-port-scan.py +++ b/plugins/guess-port-scan.py @@ -1,4 +1,5 @@ -from autorecon import PortScan, Service +from autorecon.plugins import PortScan +from autorecon.targets import Service import re class GuesPortScan(PortScan): diff --git a/plugins/http.py b/plugins/http.py index 0e08e27..03b43d6 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -1,4 +1,5 @@ -from autorecon import ServiceScan, error, info, fformat +from autorecon.plugins import ServiceScan +from autorecon.io import error, info, fformat from shutil import which import os diff --git a/plugins/kerberos.py b/plugins/kerberos.py index 488c615..0830ff5 100644 --- a/plugins/kerberos.py +++ b/plugins/kerberos.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapKerberos(ServiceScan): diff --git a/plugins/ldap.py b/plugins/ldap.py index b82f322..8f01f4f 100644 --- a/plugins/ldap.py +++ b/plugins/ldap.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapLDAP(ServiceScan): diff --git a/plugins/misc.py b/plugins/misc.py index f33f95b..20d3615 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -1,4 +1,5 @@ -from autorecon import ServiceScan, fformat +from autorecon.plugins import ServiceScan +from autorecon.io import fformat class NmapCassandra(ServiceScan): diff --git a/plugins/nfs.py b/plugins/nfs.py index 7d7c3f5..515a2ec 100644 --- a/plugins/nfs.py +++ b/plugins/nfs.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapNFS(ServiceScan): diff --git a/plugins/rdp.py b/plugins/rdp.py index 4dfc78e..e99334f 100644 --- a/plugins/rdp.py +++ b/plugins/rdp.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapRDP(ServiceScan): diff --git a/plugins/redis.py b/plugins/redis.py index 933d0f7..bbdfbc1 100644 --- a/plugins/redis.py +++ b/plugins/redis.py @@ -1,4 +1,5 @@ -from autorecon import ServiceScan, error +from autorecon.plugins import ServiceScan +from autorecon.io import error from shutil import which class NmapRedis(ServiceScan): diff --git a/plugins/rpc.py b/plugins/rpc.py index 25a3c1b..8edcec4 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -1,4 +1,5 @@ -from autorecon import ServiceScan, error, warn +from autorecon.plugins import ServiceScan +from autorecon.io import error, warn from shutil import which class NmapRPC(ServiceScan): diff --git a/plugins/rsync.py b/plugins/rsync.py index 2b4b44e..da9a4be 100644 --- a/plugins/rsync.py +++ b/plugins/rsync.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapRsync(ServiceScan): diff --git a/plugins/sip.py b/plugins/sip.py index b69ef25..8b60f7e 100644 --- a/plugins/sip.py +++ b/plugins/sip.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapSIP(ServiceScan): diff --git a/plugins/smb.py b/plugins/smb.py index af86618..e5319ab 100644 --- a/plugins/smb.py +++ b/plugins/smb.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapSMB(ServiceScan): diff --git a/plugins/smtp.py b/plugins/smtp.py index 3ced44e..705c154 100644 --- a/plugins/smtp.py +++ b/plugins/smtp.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapSMTP(ServiceScan): diff --git a/plugins/snmp.py b/plugins/snmp.py index 86a4507..7b104a0 100644 --- a/plugins/snmp.py +++ b/plugins/snmp.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapSNMP(ServiceScan): diff --git a/plugins/ssh.py b/plugins/ssh.py index aa5d1ec..2ecedab 100644 --- a/plugins/ssh.py +++ b/plugins/ssh.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class NmapSSH(ServiceScan): diff --git a/plugins/sslscan.py b/plugins/sslscan.py index 88ae41c..43071ac 100644 --- a/plugins/sslscan.py +++ b/plugins/sslscan.py @@ -1,4 +1,4 @@ -from autorecon import ServiceScan +from autorecon.plugins import ServiceScan class SSLScan(ServiceScan): From e745ee4478bc320328a51d7f55b76cebf8630180 Mon Sep 17 00:00:00 2001 From: blockomat2100 Date: Fri, 3 Sep 2021 13:07:22 +0200 Subject: [PATCH 68/95] make plugins run that uses match_all_service_names (e.g. sslscan) --- autorecon/plugins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autorecon/plugins.py b/autorecon/plugins.py index 666bb94..8d5c518 100644 --- a/autorecon/plugins.py +++ b/autorecon/plugins.py @@ -190,6 +190,8 @@ class ServiceScan(Plugin): @final def match_all_service_names(self, boolean): self.match_all_service_names_boolean = boolean + # we need at least one service name to enter loop in autorecon and make plugin finally run + self.match_service_name('.*') class AutoRecon(object): From 3974042ca033640aca01b0b48df9948dd93f1ddf Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 3 Sep 2021 23:10:11 -0400 Subject: [PATCH 69/95] Added open port alerts. --- autorecon.py | 16 +++++++++------- autorecon/targets.py | 3 ++- plugins/default-port-scan.py | 30 ++++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/autorecon.py b/autorecon.py index 664792c..28a0e22 100644 --- a/autorecon.py +++ b/autorecon.py @@ -436,7 +436,7 @@ async def scan_target(target): else: continue - info('Found {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') + info('Identified service {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') if not config['only_scans_dir']: with open(os.path.join(target.reportdir, 'notes.txt'), 'a') as file: @@ -1026,9 +1026,9 @@ async def main(): continue if isinstance(ip, ipaddress.IPv4Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv4', 'ip', autorecon)) + autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv4', 'ip', autorecon)) elif isinstance(ip, ipaddress.IPv6Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv6', 'ip', autorecon)) + autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv6', 'ip', autorecon)) else: fail('This should never happen unless IPv8 is invented.') except ValueError: @@ -1052,9 +1052,9 @@ async def main(): continue if isinstance(ip, ipaddress.IPv4Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv4', 'ip', autorecon)) + autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv4', 'ip', autorecon)) elif isinstance(ip, ipaddress.IPv6Address): - autorecon.pending_targets.append(Target(ip_str, 'IPv6', 'ip', autorecon)) + autorecon.pending_targets.append(Target(ip_str, ip_str, 'IPv6', 'ip', autorecon)) else: fail('This should never happen unless IPv8 is invented.') @@ -1062,6 +1062,7 @@ async def main(): try: addresses = socket.getaddrinfo(target, None, socket.AF_INET) + ip = addresses[0][4][0] found = False for t in autorecon.pending_targets: @@ -1072,10 +1073,11 @@ async def main(): if found: continue - autorecon.pending_targets.append(Target(target, 'IPv4', 'hostname', autorecon)) + autorecon.pending_targets.append(Target(target, ip, 'IPv4', 'hostname', autorecon)) except socket.gaierror: try: addresses = socket.getaddrinfo(target, None, socket.AF_INET6) + ip = addresses[0][4][0] found = False for t in autorecon.pending_targets: @@ -1086,7 +1088,7 @@ async def main(): if found: continue - autorecon.pending_targets.append(Target(target, 'IPv6', 'hostname', autorecon)) + autorecon.pending_targets.append(Target(target, ip, 'IPv6', 'hostname', autorecon)) except socket.gaierror: error(target + ' does not appear to be a valid IP address, IP range, or resolvable hostname.') errors = True diff --git a/autorecon/targets.py b/autorecon/targets.py index 49acc75..4d7512e 100644 --- a/autorecon/targets.py +++ b/autorecon/targets.py @@ -5,8 +5,9 @@ from autorecon.io import e, info class Target: - def __init__(self, address, ipversion, type, autorecon): + def __init__(self, address, ip, ipversion, type, autorecon): self.address = address + self.ip = ip self.ipversion = ipversion self.type = type self.autorecon = autorecon diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index 0246d3a..137d950 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -1,6 +1,6 @@ from autorecon.plugins import PortScan -from autorecon.io import error -import os +from autorecon.io import info, error +import os, re class QuickTCPPortScan(PortScan): @@ -36,7 +36,18 @@ class AllTCPPortScan(PortScan): if target.ports: # Don't run this plugin if there are custom ports. return [] process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) - services = await target.extract_services(stdout) + services = [] + while True: + line = await stdout.readline() + if line is not None: + match = re.search('^Discovered open port ([0-9]+)/tcp', line) + if match: + info('Discovered open port {bmagenta}tcp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}') + service = target.extract_service(line) + if service: + services.append(service) + else: + break await process.wait() return services @@ -59,7 +70,18 @@ class Top100UDPPortScan(PortScan): return [] else: process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all --top-ports 100 -oN "{scandir}/_top_100_udp_nmap.txt" -oX "{scandir}/xml/_top_100_udp_nmap.xml" {address}', blocking=False) - services = await target.extract_services(stdout) + services = [] + while True: + line = await stdout.readline() + if line is not None: + match = re.search('^Discovered open port ([0-9]+)/udp', line) + if match: + info('Discovered open port {bmagenta}udp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}') + service = target.extract_service(line) + if service: + services.append(service) + else: + break await process.wait() return services else: From 67fefce2a0f86557cdd0bc949579fb0e44bcb2ff Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Fri, 3 Sep 2021 23:50:40 -0400 Subject: [PATCH 70/95] Added ipaddress and ipaddressv6 format tags. --- autorecon.py | 12 ++++++++++-- autorecon/targets.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/autorecon.py b/autorecon.py index 28a0e22..fe4db14 100644 --- a/autorecon.py +++ b/autorecon.py @@ -246,6 +246,8 @@ async def service_scan(plugin, service): # Create variables for fformat references. address = service.target.address addressv6 = service.target.address + ipaddress = target.ip + ipaddressv6 = target.ip scandir = service.target.scandir protocol = service.protocol port = service.port @@ -263,7 +265,9 @@ async def service_scan(plugin, service): if service.target.ipversion == 'IPv6': nmap_extra += ' -6' - addressv6 = '[' + addressv6 + ']' + if addressv6 == target.ip: + addressv6 = '[' + addressv6 + ']' + ipaddressv6 = '[' + ipaddressv6 + ']' tag = service.tag() + '/' + plugin.slug @@ -447,6 +451,8 @@ async def scan_target(target): # Create variables for command references. address = target.address addressv6 = target.address + ipaddress = target.ip + ipaddressv6 = target.ip scandir = target.scandir protocol = service.protocol port = service.port @@ -463,7 +469,9 @@ async def scan_target(target): if target.ipversion == 'IPv6': nmap_extra += ' -6' - addressv6 = '[' + addressv6 + ']' + if addressv6 == target.ip: + addressv6 = '[' + addressv6 + ']' + ipaddressv6 = '[' + ipaddressv6 + ']' service_match = False matching_plugins = [] diff --git a/autorecon/targets.py b/autorecon/targets.py index 4d7512e..351d983 100644 --- a/autorecon/targets.py +++ b/autorecon/targets.py @@ -37,6 +37,8 @@ class Target: # Create variables for command references. address = target.address addressv6 = target.address + ipaddress = target.ip + ipaddressv6 = target.ip scandir = target.scandir nmap_extra = target.autorecon.args.nmap @@ -45,7 +47,9 @@ class Target: if target.ipversion == 'IPv6': nmap_extra += ' -6' - addressv6 = '[' + addressv6 + ']' + if addressv6 == target.ip: + addressv6 = '[' + addressv6 + ']' + ipaddressv6 = '[' + ipaddressv6 + ']' plugin = inspect.currentframe().f_back.f_locals['self'] @@ -117,6 +121,8 @@ class Service: # Create variables for command references. address = target.address addressv6 = target.address + ipaddress = target.ip + ipaddressv6 = target.ip scandir = target.scandir protocol = self.protocol port = self.port @@ -139,7 +145,9 @@ class Service: if target.ipversion == 'IPv6': nmap_extra += ' -6' - addressv6 = '[' + addressv6 + ']' + if addressv6 == target.ip: + addressv6 = '[' + addressv6 + ']' + ipaddressv6 = '[' + ipaddressv6 + ']' plugin = inspect.currentframe().f_back.f_locals['self'] From d9deb6ddd5c4f252c4dddd9dd1ba4f9367c1fb32 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 4 Sep 2021 00:37:37 -0400 Subject: [PATCH 71/95] Bug fixes and feature update. Added optional check() function for plugins to check pre-requisites, etc. after plugins are fully loaded and options have been parsed. Fixed bug in recent commit. --- autorecon.py | 12 +++++++++--- plugins/databases.py | 8 ++++++++ plugins/http.py | 18 ++++++++++++++++-- plugins/redis.py | 6 ++++-- plugins/rpc.py | 9 ++------- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/autorecon.py b/autorecon.py index fe4db14..8d7f8a4 100644 --- a/autorecon.py +++ b/autorecon.py @@ -246,8 +246,8 @@ async def service_scan(plugin, service): # Create variables for fformat references. address = service.target.address addressv6 = service.target.address - ipaddress = target.ip - ipaddressv6 = target.ip + ipaddress = service.target.ip + ipaddressv6 = service.target.ip scandir = service.target.scandir protocol = service.protocol port = service.port @@ -265,7 +265,7 @@ async def service_scan(plugin, service): if service.target.ipversion == 'IPv6': nmap_extra += ' -6' - if addressv6 == target.ip: + if addressv6 == service.target.ip: addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' @@ -863,6 +863,12 @@ async def main(): sys.exit(0) + for plugin in autorecon.plugins.values(): + for member_name, _ in inspect.getmembers(plugin, predicate=inspect.ismethod): + if member_name == 'check': + plugin.check() + continue + if config['ports']: ports_to_scan = {'tcp':[], 'udp':[]} unique = {'tcp':[], 'udp':[]} diff --git a/plugins/databases.py b/plugins/databases.py index dab52a6..046e31a 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -73,6 +73,10 @@ class OracleTNScmd(ServiceScan): def configure(self): self.match_service_name('^oracle') + def check(self): + if which('tnscmd10g') is None: + error('The tnscmd10g program could not be found. Make sure it is installed. (On Kali, run: sudo apt install tnscmd10g)') + async def run(self, service): if service.target.ipversion == 'IPv4': await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') @@ -88,6 +92,10 @@ class OracleScanner(ServiceScan): def configure(self): self.match_service_name('^oracle') + def check(self): + if which('oscanner') is None: + error('The oscanner program could not be found. Make sure it is installed. (On Kali, run: sudo apt install oscanner)') + async def run(self, service): await service.execute('oscanner -v -s {address} -P {port} 2>&1', outfile='{protocol}_{port}_oracle_scanner.txt') diff --git a/plugins/http.py b/plugins/http.py index 03b43d6..7a32d40 100644 --- a/plugins/http.py +++ b/plugins/http.py @@ -95,6 +95,18 @@ class DirBuster(ServiceScan): self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) + def check(self): + tool = self.get_option('tool') + if tool == 'feroxbuster': + if which('feroxbuster') is None: + error('The feroxbuster program could not be found. Make sure it is installed. (On Kali, run: sudo apt install feroxbuster)') + elif tool == 'gobuster': + if which('gobuster') is None: + error('The gobuster program could not be found. Make sure it is installed. (On Kali, run: sudo apt install gobuster)') + elif tool == 'dirsearch': + if which('dirsearch') is None: + error('The dirsearch program could not be found. Make sure it is installed. (On Kali, run: sudo apt install dirsearch)') + async def run(self, service): dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) for wordlist in self.get_option('wordlist'): @@ -178,12 +190,14 @@ class WkHTMLToImage(ServiceScan): self.match_service_name('^http') self.match_service_name('^nacn_http$', negative_match=True) + def check(self): + if which('wkhtmltoimage') is None: + error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') + async def run(self, service): if which('wkhtmltoimage') is not None: if service.protocol == 'tcp': await service.execute('wkhtmltoimage --format png {http_scheme}://{addressv6}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') - else: - error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') class WPScan(ServiceScan): diff --git a/plugins/redis.py b/plugins/redis.py index bbdfbc1..4603904 100644 --- a/plugins/redis.py +++ b/plugins/redis.py @@ -25,11 +25,13 @@ class RedisCli(ServiceScan): def configure(self): self.match_service_name('^redis$') + def check(self): + if which('redis-cli') is None: + error('The redis-cli program could not be found. Make sure it is installed. (On Kali, run: sudo apt install redis-tools)') + async def run(self, service): if which('redis-cli') is not None: _, stdout, _ = await service.execute('redis-cli -p {port} -h {address} INFO', outfile='{protocol}_{port}_redis_info.txt') if not (await stdout.readline()).startswith('NOAUTH Authentication required'): await service.execute('redis-cli -p {port} -h {address} CONFIG GET \'*\'', outfile='{protocol}_{port}_redis_config.txt') await service.execute('redis-cli -p {port} -h {address} CLIENT LIST', outfile='{protocol}_{port}_redis_client-list.txt') - else: - error('The redis-cli program could not be found. Make sure it is installed. (On Kali, run: sudo apt install redis-tools)') diff --git a/plugins/rpc.py b/plugins/rpc.py index 8edcec4..7880bbe 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -37,12 +37,7 @@ class RPCDump(ServiceScan): def configure(self): self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) - if which('impacket-rpcdump') is None: - warn('The impacket-rpcdump program could not be found. Some plugins may fail. (On Kali, run: sudo apt install impacket-scripts)') async def run(self, service): - if which('impacket-rpcdump') is not None: - if service.protocol == 'tcp': - await service.execute('impacket-rpcdump -port {port} {address}', outfile='{protocol}_{port}_rpc_rpcdump.txt') - else: - error('The impacket-rpcdump program could not be found. (On Kali, run: sudo apt install impacket-scripts)') + if service.protocol == 'tcp': + await service.execute('impacket-rpcdump -port {port} {address}', outfile='{protocol}_{port}_rpc_rpcdump.txt') From 9ca431f93eb88e06a7bb91123b59ddb99d73d9f5 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 4 Sep 2021 00:39:50 -0400 Subject: [PATCH 72/95] Bug fixes. Added required library import. --- plugins/databases.py | 1 + plugins/rpc.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/databases.py b/plugins/databases.py index 046e31a..992cd47 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -1,4 +1,5 @@ from autorecon.plugins import ServiceScan +from shutil import which class NmapMongoDB(ServiceScan): diff --git a/plugins/rpc.py b/plugins/rpc.py index 7880bbe..429c499 100644 --- a/plugins/rpc.py +++ b/plugins/rpc.py @@ -1,6 +1,5 @@ from autorecon.plugins import ServiceScan from autorecon.io import error, warn -from shutil import which class NmapRPC(ServiceScan): From 1d7fd227213ef5ebdc281a1943b66d8a240dfbb5 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 4 Sep 2021 00:41:02 -0400 Subject: [PATCH 73/95] Update databases.py Fixed missing import. --- plugins/databases.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/databases.py b/plugins/databases.py index 992cd47..4e5286b 100644 --- a/plugins/databases.py +++ b/plugins/databases.py @@ -1,4 +1,5 @@ from autorecon.plugins import ServiceScan +from autorecon.io import error from shutil import which class NmapMongoDB(ServiceScan): From 6525d77836cffef8e30dbac27c06b581a6e47c35 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 5 Sep 2021 12:53:17 -0400 Subject: [PATCH 74/95] Added simple proxychains support. Command line option --proxychains will add -sT to Nmap scans. There is no other logic. At some point there should be checks for each plugin. --- autorecon.py | 7 +++++++ autorecon/config.py | 3 +++ autorecon/targets.py | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/autorecon.py b/autorecon.py index 8d7f8a4..e9fdcde 100644 --- a/autorecon.py +++ b/autorecon.py @@ -269,6 +269,9 @@ async def service_scan(plugin, service): addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' + if config['proxychains']: + nmap_extra += ' -sT' + tag = service.tag() + '/' + plugin.slug info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') @@ -473,6 +476,9 @@ async def scan_target(target): addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' + if config['proxychains']: + nmap_extra += ' -sT' + service_match = False matching_plugins = [] heading = False @@ -641,6 +647,7 @@ async def main(): nmap_group = parser.add_mutually_exclusive_group() nmap_group.add_argument('--nmap', action='store', help='Override the {nmap_extra} variable in scans. Default: %(default)s') nmap_group.add_argument('--nmap-append', action='store', help='Append to the default {nmap_extra} variable in scans. Default: %(default)s') + parser.add_argument('--proxychains', action='store_true', help='Use if you are running AutoRecon via proxychains. Default: %(default)s') parser.add_argument('--disable-sanity-checks', action='store_true', help='Disable sanity checks that would otherwise prevent the scans from running. Default: %(default)s') parser.add_argument('--disable-keyboard-control', action='store_true', help='Disables keyboard control ([s]tatus, Up, Down) if you are in SSH or Docker.') parser.add_argument('--force-services', action='store', nargs='+', help='A space separated list of services in the following style: tcp/80/http/insecure tcp/443/https/secure') diff --git a/autorecon/config.py b/autorecon/config.py index 5352c39..26ec78f 100644 --- a/autorecon/config.py +++ b/autorecon/config.py @@ -17,6 +17,7 @@ configurable_keys = [ 'target_timeout', 'nmap', 'nmap_append', + 'proxychains', 'disable_sanity_checks', 'disable_keyboard_control', 'force_services', @@ -28,6 +29,7 @@ configurable_boolean_keys = [ 'single_target', 'only_scans_dir', 'create_port_dirs', + 'proxychains', 'disable_sanity_checks', 'accessible' ] @@ -51,6 +53,7 @@ config = { 'target_timeout': None, 'nmap': '-vv --reason -Pn', 'nmap_append': '', + 'proxychains': False, 'disable_sanity_checks': False, 'disable_keyboard_control': False, 'force_services': None, diff --git a/autorecon/targets.py b/autorecon/targets.py index 351d983..b4aba56 100644 --- a/autorecon/targets.py +++ b/autorecon/targets.py @@ -51,6 +51,9 @@ class Target: addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' + if config['proxychains']: + nmap_extra += ' -sT' + plugin = inspect.currentframe().f_back.f_locals['self'] cmd = e(cmd) @@ -149,6 +152,9 @@ class Service: addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' + if config['proxychains']: + nmap_extra += ' -sT' + plugin = inspect.currentframe().f_back.f_locals['self'] cmd = e(cmd) From f73fb32e3de2cc37bb8df6349e31f563f66f09a0 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Mon, 6 Sep 2021 09:23:38 -0400 Subject: [PATCH 75/95] Update config.toml Added -T4 by default to nmap scans. --- config.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/config.toml b/config.toml index 0df214e..a9df31d 100644 --- a/config.toml +++ b/config.toml @@ -1,6 +1,7 @@ # Configure regular AutoRecon options at the top of this file. create-port-dirs = true +nmap-append = '-T4' # verbose = 1 # max-scans = 30 From 55b928ac6430dd6d9af7e9a03392294a568874bd Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 8 Sep 2021 00:27:16 -0400 Subject: [PATCH 76/95] Added Report Plugin functionality. Moved http.py to http_server.py to avoid import clashes with Python's http library. Report plugins can take a list of targets and create reports based on the scans and files. --- autorecon.py | 212 ++++++++++++++++++++-------- autorecon/io.py | 4 + autorecon/plugins.py | 16 ++- autorecon/targets.py | 23 ++- plugins/{http.py => http_server.py} | 2 +- plugins/reporting.py | 88 ++++++++++++ 6 files changed, 275 insertions(+), 70 deletions(-) rename plugins/{http.py => http_server.py} (99%) create mode 100644 plugins/reporting.py diff --git a/autorecon.py b/autorecon.py index e9fdcde..2db5df7 100644 --- a/autorecon.py +++ b/autorecon.py @@ -12,7 +12,7 @@ colorama.init() from autorecon.config import config, configurable_keys, configurable_boolean_keys from autorecon.io import slugify, e, fformat, cprint, debug, info, warn, error, fail, CommandStreamReader -from autorecon.plugins import Pattern, PortScan, ServiceScan, AutoRecon +from autorecon.plugins import Pattern, PortScan, ServiceScan, Report, AutoRecon from autorecon.targets import Target, Service # Save current terminal settings so we can restore them. @@ -141,6 +141,40 @@ async def keyboard(): input = input[1:] await asyncio.sleep(0.1) +async def get_semaphore(autorecon): + semaphore = autorecon.service_scan_semaphore + while True: + # If service scan semaphore is locked, see if we can use port scan semaphore. + if semaphore.locked(): + if semaphore != autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans + + port_scan_task_count = 0 + for target in autorecon.scanning_targets: + for process_list in target.running_tasks.values(): + if issubclass(process_list['plugin'].__class__, PortScan): + port_scan_task_count += 1 + + if not autorecon.pending_targets and (config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. + if autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = autorecon.port_scan_semaphore + break + else: # Do some math to see if we can use the port scan semaphore. + if (config['max_port_scans'] - (port_scan_task_count + (len(autorecon.pending_targets) * config['port_scan_plugin_count']))) >= 1: + if autorecon.port_scan_semaphore.locked(): + await asyncio.sleep(1) + continue + semaphore = autorecon.port_scan_semaphore + break + else: + await asyncio.sleep(1) + else: + break + else: + break + return semaphore + async def port_scan(plugin, target): if config['ports']: if config['ports']['tcp'] or config['ports']['udp']: @@ -207,40 +241,10 @@ async def port_scan(plugin, target): return {'type':'port', 'plugin':plugin, 'result':result} async def service_scan(plugin, service): - #from autorecon import PortScan semaphore = service.target.autorecon.service_scan_semaphore if not config['force_services']: - # If service scan semaphore is locked, see if we can use port scan semaphore. - while True: - if semaphore.locked(): - if semaphore != service.target.autorecon.port_scan_semaphore: # This will be true unless user sets max_scans == max_port_scans - - port_scan_task_count = 0 - for targ in service.target.autorecon.scanning_targets: - for process_list in targ.running_tasks.values(): - if issubclass(process_list['plugin'].__class__, PortScan): - port_scan_task_count += 1 - - if not service.target.autorecon.pending_targets and (config['max_port_scans'] - port_scan_task_count) >= 1: # If no more targets, and we have room, use port scan semaphore. - if service.target.autorecon.port_scan_semaphore.locked(): - await asyncio.sleep(1) - continue - semaphore = service.target.autorecon.port_scan_semaphore - break - else: # Do some math to see if we can use the port scan semaphore. - if (config['max_port_scans'] - (port_scan_task_count + (len(service.target.autorecon.pending_targets) * config['port_scan_plugin_count']))) >= 1: - if service.target.autorecon.port_scan_semaphore.locked(): - await asyncio.sleep(1) - continue - semaphore = service.target.autorecon.port_scan_semaphore - break - else: - await asyncio.sleep(1) - else: - break - else: - break + semaphore = await get_semaphore(service.target.autorecon) async with semaphore: # Create variables for fformat references. @@ -253,6 +257,11 @@ async def service_scan(plugin, service): port = service.port name = service.name + if config['create_port_dirs']: + scandir = os.path.join(scandir, protocol + str(port)) + os.makedirs(scandir, exist_ok=True) + os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) + # Special cases for HTTP. http_scheme = 'https' if 'https' in service.name or service.secure is True else 'http' @@ -319,6 +328,20 @@ async def service_scan(plugin, service): info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time) return {'type':'service', 'plugin':plugin, 'result':result} +async def generate_report(plugin, targets): + semaphore = autorecon.service_scan_semaphore + + if not config['force_services']: + semaphore = await get_semaphore(autorecon) + + async with semaphore: + try: + result = await plugin.run(targets) + except Exception as ex: + exc_type, exc_value, exc_tb = sys.exc_info() + error_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)[-2:]) + raise Exception(cprint('Error: Report plugin {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} produced an exception:\n\n' + error_text, color=Fore.RED, char='!', printmsg=False)) + async def scan_target(target): os.makedirs(os.path.abspath(config['outdir']), exist_ok=True) @@ -330,6 +353,12 @@ async def scan_target(target): target.basedir = basedir + scandir = os.path.join(basedir, 'scans') + target.scandir = scandir + os.makedirs(scandir, exist_ok=True) + + os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) + if not config['only_scans_dir']: exploitdir = os.path.join(basedir, 'exploit') os.makedirs(exploitdir, exist_ok=True) @@ -338,7 +367,6 @@ async def scan_target(target): os.makedirs(lootdir, exist_ok=True) reportdir = os.path.join(basedir, 'report') - target.reportdir = reportdir os.makedirs(reportdir, exist_ok=True) open(os.path.join(reportdir, 'local.txt'), 'a').close() @@ -346,12 +374,10 @@ async def scan_target(target): screenshotdir = os.path.join(reportdir, 'screenshots') os.makedirs(screenshotdir, exist_ok=True) + else: + reportdir = scandir - scandir = os.path.join(basedir, 'scans') - target.scandir = scandir - os.makedirs(scandir, exist_ok=True) - - os.makedirs(os.path.join(scandir, 'xml'), exist_ok=True) + target.reportdir = reportdir pending = [] @@ -397,6 +423,7 @@ async def scan_target(target): break if matching_tags and not excluded_tags: + target.scans['ports'][plugin.slug] = {'plugin':plugin, 'commands':[]} pending.append(asyncio.create_task(port_scan(plugin, target))) async with autorecon.lock: @@ -528,9 +555,15 @@ async def scan_target(target): if plugin_is_runnable and matching_tags and not excluded_tags: # Skip plugin if run_once_boolean and plugin already in target scans - if plugin.run_once_boolean and (plugin.slug,) in target.scans: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') - break + if plugin.run_once_boolean: + plugin_queued = False + for s in target.scans['services']: + if plugin.slug in target.scans['services'][s]: + plugin_queued = True + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') + break + if plugin_queued: + break # Skip plugin if require_ssl_boolean and port is not secure if plugin.require_ssl_boolean and not service.secure: @@ -562,16 +595,22 @@ async def scan_target(target): if member_name == 'manual': plugin.manual(service, plugin_was_run) - if service.manual_commands and (not plugin.run_once_boolean or (plugin.run_once_boolean and (plugin.slug,) not in target.scans)): - with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: - if not heading: - file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) - heading = True - for description, commands in service.manual_commands.items(): - file.write('\t[-] ' + e(description) + '\n\n') - for command in commands: - file.write('\t\t' + e(command) + '\n\n') - file.flush() + if service.manual_commands: + plugin_run = False + for s in target.scans['services']: + if plugin.slug in target.scans['services'][s]: + plugin_run = True + break + if not plugin.run_once_boolean or (plugin.run_once_boolean and not plugin_run): + with open(os.path.join(scandir, '_manual_commands.txt'), 'a') as file: + if not heading: + file.write(e('[*] {service.name} on {service.protocol}/{service.port}\n\n')) + heading = True + for description, commands in service.manual_commands.items(): + file.write('\t[-] ' + e(description) + '\n\n') + for command in commands: + file.write('\t\t' + e(command) + '\n\n') + file.flush() service.manual_commands = {} break @@ -584,15 +623,23 @@ async def scan_target(target): for plugin in matching_plugins: plugin_tag = service.tag() + '/' + plugin.slug - scan_tuple = (service.protocol, service.port, service.name, plugin.slug) if plugin.run_once_boolean: - scan_tuple = (plugin.slug,) + plugin_tag = plugin.slug - if scan_tuple in target.scans: - warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') + plugin_queued = False + if service in target.scans['services']: + for s in target.scans['services']: + if plugin_tag in target.scans['services'][s]: + plugin_queued = True + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') + break + + if plugin_queued: continue else: - target.scans.append(scan_tuple) + if service not in target.scans['services']: + target.scans['services'][service] = {} + target.scans['services'][service][plugin_tag] = {'plugin':plugin, 'commands':[]} pending.add(asyncio.create_task(service_scan(plugin, service))) @@ -601,6 +648,27 @@ async def scan_target(target): if service.full_tag() not in target.autorecon.missing_services: target.autorecon.missing_services.append(service.full_tag()) + for plugin in target.autorecon.plugin_types['report']: + plugin_tag_set = set(plugin.tags) + + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break + + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break + + if matching_tags and not excluded_tags: + pending.add(asyncio.create_task(generate_report(plugin, [target]))) + + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + heartbeat.cancel() elapsed_time = calculate_elapsed_time(start_time) @@ -621,6 +689,7 @@ async def scan_target(target): info('Finished scanning target {byellow}' + target.address + '{rst} in ' + elapsed_time) async with autorecon.lock: + autorecon.completed_targets.append(target) autorecon.scanning_targets.remove(target) async def main(): @@ -726,11 +795,11 @@ async def main(): print('Plugin "' + c.__name__ + '" in ' + filename + ' is using a protected class name. Please change it.') sys.exit(1) - # Only add classes that are a sub class of either PortScan or ServiceScan - if issubclass(c, PortScan) or issubclass(c, ServiceScan): + # Only add classes that are a sub class of either PortScan, ServiceScan, or Report + if issubclass(c, PortScan) or issubclass(c, ServiceScan) or issubclass(c, Report): autorecon.register(c(), filename) else: - print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan or ServiceScan.') + print('Plugin "' + c.__name__ + '" in ' + filename + ' is not a subclass of either PortScan, ServiceScan, or Report.') except (ImportError, SyntaxError) as ex: print('cannot import ' + filename + ' plugin') print(ex) @@ -748,6 +817,7 @@ async def main(): # Sort plugins by priority. autorecon.plugin_types['port'].sort(key=lambda x: x.priority) autorecon.plugin_types['service'].sort(key=lambda x: x.priority) + autorecon.plugin_types['report'].sort(key=lambda x: x.priority) if not os.path.isfile(config['global_file']): fail('Error: Specified global file "' + config['global_file'] + '" does not exist.') @@ -867,6 +937,9 @@ async def main(): if type in ['plugin', 'plugins', 'service', 'services', 'servicescan', 'servicescans']: for p in autorecon.plugin_types['service']: print('ServiceScan: ' + p.name + ' (' + p.slug + ')' + (' - ' + p.description if p.description else '')) + if type in ['plugin', 'plugins', 'report', 'reporting']: + for p in autorecon.plugin_types['report']: + print('Report: ' + p.name + ' (' + p.slug + ')' + (' - ' + p.description if p.description else '')) sys.exit(0) @@ -1216,6 +1289,27 @@ async def main(): keyboard_monitor.cancel() + for plugin in autorecon.plugin_types['report']: + plugin_tag_set = set(plugin.tags) + + matching_tags = False + for tag_group in autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break + + excluded_tags = False + for tag_group in autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break + + if matching_tags and not excluded_tags: + pending.add(asyncio.create_task(generate_report(plugin, autorecon.completed_targets))) + + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + if timed_out: cancel_all_tasks(None, None) diff --git a/autorecon/io.py b/autorecon/io.py index ec15f0e..5d0c574 100644 --- a/autorecon/io.py +++ b/autorecon/io.py @@ -104,6 +104,10 @@ class CommandStreamReader(object): self.outfile = outfile self.ended = False + # Empty files that already exist. + if self.outfile != None: + with open(self.outfile, 'w'): pass + # Read lines from the stream until it ends. async def _read(self): while True: diff --git a/autorecon/plugins.py b/autorecon/plugins.py index 666bb94..030b9b6 100644 --- a/autorecon/plugins.py +++ b/autorecon/plugins.py @@ -191,14 +191,20 @@ class ServiceScan(Plugin): def match_all_service_names(self, boolean): self.match_all_service_names_boolean = boolean +class Report(Plugin): + + def __init__(self): + super().__init__() + class AutoRecon(object): def __init__(self): self.pending_targets = [] self.scanning_targets = [] + self.completed_targets = [] self.plugins = {} self.__slug_regex = re.compile('^[a-z0-9\-]+$') - self.plugin_types = {'port':[], 'service':[]} + self.plugin_types = {'port':[], 'service':[], 'report':[]} self.port_scan_semaphore = None self.service_scan_semaphore = None self.argparse = None @@ -259,6 +265,9 @@ class AutoRecon(object): if plugin.disabled: return + if plugin.name is None: + fail('Error: Plugin with class name "' + plugin.__class__.__name__ + '" in ' + filename + ' does not have a name.') + for _, loaded_plugin in self.plugins.items(): if plugin.name == loaded_plugin.name: fail('Error: Duplicate plugin name "' + plugin.name + '" detected in ' + filename + '.', file=sys.stderr) @@ -296,13 +305,14 @@ class AutoRecon(object): if not run_coroutine_found and not manual_function_found: fail('Error: the plugin "' + plugin.name + '" in ' + filename + ' needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) - #from autorecon import PortScan, ServiceScan if issubclass(plugin.__class__, PortScan): self.plugin_types["port"].append(plugin) elif issubclass(plugin.__class__, ServiceScan): self.plugin_types["service"].append(plugin) + elif issubclass(plugin.__class__, Report): + self.plugin_types["report"].append(plugin) else: - fail('Plugin "' + plugin.name + '" in ' + filename + ' is neither a PortScan nor a ServiceScan.', file=sys.stderr) + fail('Plugin "' + plugin.name + '" in ' + filename + ' is neither a PortScan, ServiceScan, nor a Report.', file=sys.stderr) plugin.tags = [tag.lower() for tag in plugin.tags] diff --git a/autorecon/targets.py b/autorecon/targets.py index b4aba56..40476f7 100644 --- a/autorecon/targets.py +++ b/autorecon/targets.py @@ -18,7 +18,7 @@ class Target: self.ports = None self.pending_services = [] self.services = [] - self.scans = [] + self.scans = {'ports':{}, 'services':{}} self.running_tasks = {} async def add_service(self, service): @@ -31,7 +31,7 @@ class Target: async def extract_services(self, stream, regex=None): return await self.autorecon.extract_services(stream, regex) - async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + async def execute(self, cmd, blocking=True, outfile=None, errfile=None, future_outfile=None): target = self # Create variables for command references. @@ -55,9 +55,7 @@ class Target: nmap_extra += ' -sT' plugin = inspect.currentframe().f_back.f_locals['self'] - cmd = e(cmd) - tag = plugin.slug if config['verbose'] >= 1: @@ -69,6 +67,11 @@ class Target: if errfile is not None: errfile = os.path.join(target.scandir, e(errfile)) + if future_outfile is not None: + future_outfile = os.path.join(target.scandir, e(future_outfile)) + + target.scans['ports'][tag]['commands'].append([cmd, outfile if outfile is not None else future_outfile, errfile]) + async with target.lock: with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: file.writelines(cmd + '\n\n') @@ -118,7 +121,7 @@ class Service: self.add_manual_commands(description, command) @final - async def execute(self, cmd, blocking=True, outfile=None, errfile=None): + async def execute(self, cmd, blocking=True, outfile=None, errfile=None, future_outfile=None): target = self.target # Create variables for command references. @@ -156,10 +159,11 @@ class Service: nmap_extra += ' -sT' plugin = inspect.currentframe().f_back.f_locals['self'] - cmd = e(cmd) - tag = self.tag() + '/' + plugin.slug + plugin_tag = tag + if plugin.run_once_boolean: + plugin_tag = plugin.slug if config['verbose'] >= 1: info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) @@ -170,6 +174,11 @@ class Service: if errfile is not None: errfile = os.path.join(scandir, e(errfile)) + if future_outfile is not None: + future_outfile = os.path.join(scandir, e(future_outfile)) + + target.scans['services'][self][plugin_tag]['commands'].append([cmd, outfile if outfile is not None else future_outfile, errfile]) + async with target.lock: with open(os.path.join(target.scandir, '_commands.log'), 'a') as file: file.writelines(cmd + '\n\n') diff --git a/plugins/http.py b/plugins/http_server.py similarity index 99% rename from plugins/http.py rename to plugins/http_server.py index 7a32d40..97bcc1f 100644 --- a/plugins/http.py +++ b/plugins/http_server.py @@ -68,7 +68,7 @@ class CurlRobots(ServiceScan): async def run(self, service): if service.protocol == 'tcp': - _, stdout, _ = await service.execute('curl -sSikf {http_scheme}://{addressv6}:{port}/robots.txt') + _, stdout, _ = await service.execute('curl -sSikf {http_scheme}://{addressv6}:{port}/robots.txt', future_outfile='{protocol}_{port}_{http_scheme}_curl-robots.txt') lines = await stdout.readlines() if lines: diff --git a/plugins/reporting.py b/plugins/reporting.py new file mode 100644 index 0000000..8c3bfea --- /dev/null +++ b/plugins/reporting.py @@ -0,0 +1,88 @@ +from autorecon.plugins import Report +from autorecon.config import config +from xml.sax.saxutils import escape +import os, glob + +class CherryTree(Report): + + def __init__(self): + super().__init__() + self.name = 'CherryTree' + + async def run(self, targets): + if len(targets) > 1: + report = os.path.join(config['outdir'], 'cherrytree.xml.ctd') + elif len(targets) == 1: + report = os.path.join(targets[0].reportdir, 'cherrytree.xml.ctd') + else: + return + + with open(report, 'w') as output: + output.writelines('\n\n') + for target in targets: + output.writelines('\n') + + files = [os.path.abspath(filename) for filename in glob.iglob(os.path.join(target.scandir, '**/*'), recursive=True) if os.path.isfile(filename) and filename.endswith(('.txt', '.html'))] + + if target.scans['ports']: + output.writelines('\n') + for scan in target.scans['ports'].keys(): + output.writelines('\n') + for command in target.scans['ports'][scan]['commands']: + output.writelines('' + escape(command[0])) + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n' + escape(filename) + ':\n\n') + with open(filename, 'r') as file: + output.writelines(escape(file.read()) + '\n') + output.writelines('\n') + output.writelines('\n') + output.writelines('\n') + if target.scans['services']: + output.writelines('\n') + for service in target.scans['services'].keys(): + output.writelines('\n') + for plugin in target.scans['services'][service].keys(): + output.writelines('\n') + for command in target.scans['services'][service][plugin]['commands']: + output.writelines('' + escape(command[0])) + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n' + escape(filename) + ':\n\n') + with open(filename, 'r') as file: + output.writelines(escape(file.read()) + '\n') + output.writelines('\n') + output.writelines('\n') + output.writelines('\n') + output.writelines('\n') + + manual_commands = os.path.join(target.scandir, '_manual_commands.txt') + if os.path.isfile(manual_commands): + output.writelines('\n') + with open(manual_commands, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + + patterns = os.path.join(target.scandir, '_patterns.log') + if os.path.isfile(patterns): + output.writelines('\n') + with open(patterns, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + + commands = os.path.join(target.scandir, '_commands.log') + if os.path.isfile(commands): + output.writelines('\n') + with open(commands, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + + errors = os.path.join(target.scandir, '_errors.log') + if os.path.isfile(errors): + output.writelines('\n') + with open(errors, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + output.writelines('\n') + + output.writelines('') From 12877aee35e030366cdbe5ee070a619f6c5d2268 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 8 Sep 2021 04:18:22 -0400 Subject: [PATCH 77/95] Fix for match_all_service_names Removed the boolean and resorted to simply adding a "match all" service name regular expression. --- autorecon.py | 2 +- autorecon/plugins.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/autorecon.py b/autorecon.py index 2db5df7..3e8a8a9 100644 --- a/autorecon.py +++ b/autorecon.py @@ -531,7 +531,7 @@ async def scan_target(target): if re.search(s, service.name): plugin_service_match = True - if plugin.match_all_service_names_boolean or plugin_service_match: + if plugin_service_match: plugin_tag_set = set(plugin.tags) matching_tags = False diff --git a/autorecon/plugins.py b/autorecon/plugins.py index 92e1a8e..4f97811 100644 --- a/autorecon/plugins.py +++ b/autorecon/plugins.py @@ -108,7 +108,6 @@ class ServiceScan(Plugin): self.services = [] self.service_names = [] self.ignore_service_names = [] - self.match_all_service_names_boolean = False self.run_once_boolean = False self.require_ssl_boolean = False @@ -189,9 +188,9 @@ class ServiceScan(Plugin): @final def match_all_service_names(self, boolean): - self.match_all_service_names_boolean = boolean - # we need at least one service name to enter loop in autorecon and make plugin finally run - self.match_service_name('.*') + if boolean: + # Add a "match all" service name. + self.match_service_name('.*') class Report(Plugin): From bdfc2281811f324f57a4397dd1c9c05e78b02d0c Mon Sep 17 00:00:00 2001 From: Eduardo Balsa Date: Wed, 8 Sep 2021 15:03:10 +0200 Subject: [PATCH 78/95] Added shebang line Added shebang line so that script can be executed directly --- autorecon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autorecon.py b/autorecon.py index 3e8a8a9..ee24b6a 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import argparse, asyncio, importlib, inspect, ipaddress, math, os, re, sys, signal, select, socket, termios, time, traceback, tty from datetime import datetime From 2d482110e17145854f8e03f454e8332640cff8f6 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 8 Sep 2021 21:30:07 -0400 Subject: [PATCH 79/95] Proxychains bug fixes. Removed UDP port scans from proxychains. Fixed bug where running nmap through proxychains as sudo wouldn't work. --- autorecon.py | 14 ++++++++++---- autorecon/plugins.py | 7 +++++++ autorecon/targets.py | 5 +++-- plugins/default-port-scan.py | 25 ++++++++++++++++++++----- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/autorecon.py b/autorecon.py index 3e8a8a9..0ba4e71 100644 --- a/autorecon.py +++ b/autorecon.py @@ -183,8 +183,8 @@ async def port_scan(plugin, target): target.ports['tcp'] = ','.join(config['ports']['tcp']) if config['ports']['udp']: target.ports['udp'] = ','.join(config['ports']['udp']) - if plugin.type is None: - warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} does not have a type set, and --ports was used. Skipping.') + if plugin.specific_ports is False: + warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} cannot be used to scan specific ports, and --ports was used. Skipping.') return {'type':'port', 'plugin':plugin, 'result':[]} else: if plugin.type == 'tcp' and not config['ports']['tcp']: @@ -278,7 +278,7 @@ async def service_scan(plugin, service): addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' - if config['proxychains']: + if config['proxychains'] and protocol == 'tcp': nmap_extra += ' -sT' tag = service.tag() + '/' + plugin.slug @@ -391,6 +391,9 @@ async def scan_target(target): match = re.search('(?P(tcp|udp))\/(?P\d+)\/(?P[\w\-\/]+)\/(?Psecure|insecure)', forced_service) if match: protocol = match.group('protocol') + if config['proxychains'] and protocol == 'udp': + error('The service ' + forced_service + ' uses UDP and --proxychains is enabled. Skipping.') + continue port = int(match.group('port')) service = match.group('service') secure = True if match.group('secure') == 'secure' else False @@ -408,6 +411,9 @@ async def scan_target(target): return else: for plugin in target.autorecon.plugin_types['port']: + if config['proxychains'] and plugin.type == 'udp': + continue + plugin_tag_set = set(plugin.tags) matching_tags = False @@ -503,7 +509,7 @@ async def scan_target(target): addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' - if config['proxychains']: + if config['proxychains'] and protocol == 'tcp': nmap_extra += ' -sT' service_match = False diff --git a/autorecon/plugins.py b/autorecon/plugins.py index 4f97811..e3eb295 100644 --- a/autorecon/plugins.py +++ b/autorecon/plugins.py @@ -95,6 +95,7 @@ class PortScan(Plugin): def __init__(self): super().__init__() self.type = None + self.specific_ports = False async def run(self, target): raise NotImplementedError @@ -307,6 +308,12 @@ class AutoRecon(object): fail('Error: the plugin "' + plugin.name + '" in ' + filename + ' needs either a "manual" function, a "run" coroutine, or both.', file=sys.stderr) if issubclass(plugin.__class__, PortScan): + if plugin.type is None: + fail('Error: the PortScan plugin "' + plugin.name + '" in ' + filename + ' requires a type (either tcp or udp).') + else: + plugin.type = plugin.type.lower() + if plugin.type not in ['tcp', 'udp']: + fail('Error: the PortScan plugin "' + plugin.name + '" in ' + filename + ' has an invalid type (should be tcp or udp).') self.plugin_types["port"].append(plugin) elif issubclass(plugin.__class__, ServiceScan): self.plugin_types["service"].append(plugin) diff --git a/autorecon/targets.py b/autorecon/targets.py index 40476f7..1cc03e7 100644 --- a/autorecon/targets.py +++ b/autorecon/targets.py @@ -51,10 +51,11 @@ class Target: addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' + plugin = inspect.currentframe().f_back.f_locals['self'] + if config['proxychains']: nmap_extra += ' -sT' - plugin = inspect.currentframe().f_back.f_locals['self'] cmd = e(cmd) tag = plugin.slug @@ -155,7 +156,7 @@ class Service: addressv6 = '[' + addressv6 + ']' ipaddressv6 = '[' + ipaddressv6 + ']' - if config['proxychains']: + if config['proxychains'] and protocol == 'tcp': nmap_extra += ' -sT' plugin = inspect.currentframe().f_back.f_locals['self'] diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index 137d950..f9ff930 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -1,5 +1,6 @@ from autorecon.plugins import PortScan from autorecon.io import info, error +from autorecon.config import config import os, re class QuickTCPPortScan(PortScan): @@ -9,17 +10,23 @@ class QuickTCPPortScan(PortScan): self.name = 'Top TCP Ports' self.description = 'Performs an Nmap scan of the top 1000 TCP ports.' self.type = 'tcp' + self.specific_ports = True self.tags = ['default', 'default-port-scan'] self.priority = 0 async def run(self, target): + if config['proxychains']: + traceroute_os = '' + else: + traceroute_os = ' -A --osscan-guess' + if target.ports: if target.ports['tcp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) else: return [] else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) services = await target.extract_services(stdout) await process.wait() return services @@ -30,12 +37,19 @@ class AllTCPPortScan(PortScan): super().__init__() self.name = 'All TCP Ports' self.description = 'Performs an Nmap scan of all TCP ports.' + self.type = 'tcp' self.tags = ['default', 'default-port-scan', 'long'] async def run(self, target): if target.ports: # Don't run this plugin if there are custom ports. return [] - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + + if config['proxychains']: + traceroute_os = '' + else: + traceroute_os = ' -A --osscan-guess' + + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) services = [] while True: line = await stdout.readline() @@ -58,6 +72,7 @@ class Top100UDPPortScan(PortScan): self.name = 'Top 100 UDP Ports' self.description = 'Performs an Nmap scan of the top 100 UDP ports.' self.type = 'udp' + self.specific_ports = True self.tags = ['default', 'default-port-scan', 'long'] async def run(self, target): @@ -65,11 +80,11 @@ class Top100UDPPortScan(PortScan): if os.getuid() == 0: if target.ports: if target.ports['udp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all -p ' + target.ports['udp'] + ' -oN "{scandir}/_custom_ports_udp_nmap.txt" -oX "{scandir}/xml/_custom_ports_udp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --osscan-guess -p ' + target.ports['udp'] + ' -oN "{scandir}/_custom_ports_udp_nmap.txt" -oX "{scandir}/xml/_custom_ports_udp_nmap.xml" {address}', blocking=False) else: return [] else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --version-all --top-ports 100 -oN "{scandir}/_top_100_udp_nmap.txt" -oX "{scandir}/xml/_top_100_udp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --top-ports 100 -oN "{scandir}/_top_100_udp_nmap.txt" -oX "{scandir}/xml/_top_100_udp_nmap.xml" {address}', blocking=False) services = [] while True: line = await stdout.readline() From 40eceba382f3d8815ccf86677b2a3d183e8e19b9 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Wed, 8 Sep 2021 22:00:48 -0400 Subject: [PATCH 80/95] Added Markdown Report Plugin Markdown Report Plugin made default. Added check to ensure that reports aren't generated twice. --- autorecon.py | 34 +++++++------ plugins/reporting.py | 115 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 113 insertions(+), 36 deletions(-) diff --git a/autorecon.py b/autorecon.py index 05cd4c0..2e35c99 100644 --- a/autorecon.py +++ b/autorecon.py @@ -1297,26 +1297,28 @@ async def main(): keyboard_monitor.cancel() - for plugin in autorecon.plugin_types['report']: - plugin_tag_set = set(plugin.tags) + # If there's only one target we don't need a combined report + if len(autorecon.completed_targets) > 1: + for plugin in autorecon.plugin_types['report']: + plugin_tag_set = set(plugin.tags) - matching_tags = False - for tag_group in autorecon.tags: - if set(tag_group).issubset(plugin_tag_set): - matching_tags = True - break + matching_tags = False + for tag_group in autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break - excluded_tags = False - for tag_group in autorecon.excluded_tags: - if set(tag_group).issubset(plugin_tag_set): - excluded_tags = True - break + excluded_tags = False + for tag_group in autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break - if matching_tags and not excluded_tags: - pending.add(asyncio.create_task(generate_report(plugin, autorecon.completed_targets))) + if matching_tags and not excluded_tags: + pending.add(asyncio.create_task(generate_report(plugin, autorecon.completed_targets))) - while pending: - done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED, timeout=1) if timed_out: cancel_all_tasks(None, None) diff --git a/plugins/reporting.py b/plugins/reporting.py index 8c3bfea..e5b87be 100644 --- a/plugins/reporting.py +++ b/plugins/reporting.py @@ -8,12 +8,13 @@ class CherryTree(Report): def __init__(self): super().__init__() self.name = 'CherryTree' + self.tags = [] async def run(self, targets): if len(targets) > 1: - report = os.path.join(config['outdir'], 'cherrytree.xml.ctd') + report = os.path.join(config['outdir'], 'report.xml.ctd') elif len(targets) == 1: - report = os.path.join(targets[0].reportdir, 'cherrytree.xml.ctd') + report = os.path.join(targets[0].reportdir, 'report.xml.ctd') else: return @@ -27,24 +28,9 @@ class CherryTree(Report): if target.scans['ports']: output.writelines('\n') for scan in target.scans['ports'].keys(): - output.writelines('\n') - for command in target.scans['ports'][scan]['commands']: - output.writelines('' + escape(command[0])) - for filename in files: - if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): - output.writelines('\n\n' + escape(filename) + ':\n\n') - with open(filename, 'r') as file: - output.writelines(escape(file.read()) + '\n') - output.writelines('\n') - output.writelines('\n') - output.writelines('\n') - if target.scans['services']: - output.writelines('\n') - for service in target.scans['services'].keys(): - output.writelines('\n') - for plugin in target.scans['services'][service].keys(): - output.writelines('\n') - for command in target.scans['services'][service][plugin]['commands']: + if len(target.scans['ports'][scan]['commands']) > 0: + output.writelines('\n') + for command in target.scans['ports'][scan]['commands']: output.writelines('' + escape(command[0])) for filename in files: if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): @@ -53,6 +39,23 @@ class CherryTree(Report): output.writelines(escape(file.read()) + '\n') output.writelines('\n') output.writelines('\n') + output.writelines('\n') + if target.scans['services']: + output.writelines('\n') + for service in target.scans['services'].keys(): + output.writelines('\n') + for plugin in target.scans['services'][service].keys(): + if len(target.scans['services'][service][plugin]['commands']) > 0: + output.writelines('\n') + for command in target.scans['services'][service][plugin]['commands']: + output.writelines('' + escape(command[0])) + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n' + escape(filename) + ':\n\n') + with open(filename, 'r') as file: + output.writelines(escape(file.read()) + '\n') + output.writelines('\n') + output.writelines('\n') output.writelines('\n') output.writelines('\n') @@ -86,3 +89,75 @@ class CherryTree(Report): output.writelines('\n') output.writelines('') + +class Markdown(Report): + + def __init__(self): + super().__init__() + self.name = 'Markdown' + + async def run(self, targets): + if len(targets) > 1: + report = os.path.join(config['outdir'], 'report.md') + elif len(targets) == 1: + report = os.path.join(targets[0].reportdir, 'report.md') + else: + return + + os.makedirs(report, exist_ok=True) + + for target in targets: + os.makedirs(os.path.join(report, target.address), exist_ok=True) + + files = [os.path.abspath(filename) for filename in glob.iglob(os.path.join(target.scandir, '**/*'), recursive=True) if os.path.isfile(filename) and filename.endswith(('.txt', '.html'))] + + if target.scans['ports']: + os.makedirs(os.path.join(report, target.address, 'Port Scans'), exist_ok=True) + for scan in target.scans['ports'].keys(): + if len(target.scans['ports'][scan]['commands']) > 0: + with open(os.path.join(report, target.address, 'Port Scans', 'PortScan - ' + target.scans['ports'][scan]['plugin'].name + '.md'), 'w') as output: + for command in target.scans['ports'][scan]['commands']: + output.writelines('```bash\n' + command[0] + '\n```') + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n[' + filename + '](file://' + filename + '):\n\n') + with open(filename, 'r') as file: + output.writelines('```\n' + file.read() + '\n```\n') + if target.scans['services']: + os.makedirs(os.path.join(report, target.address, 'Services'), exist_ok=True) + for service in target.scans['services'].keys(): + os.makedirs(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-')), exist_ok=True) + for plugin in target.scans['services'][service].keys(): + if len(target.scans['services'][service][plugin]['commands']) > 0: + with open(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-'), target.scans['services'][service][plugin]['plugin'].name + '.md'), 'w') as output: + for command in target.scans['services'][service][plugin]['commands']: + output.writelines('```bash\n' + command[0] + '\n```') + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n[' + filename + '](file://' + filename + '):\n\n') + with open(filename, 'r') as file: + output.writelines('```\n' + file.read() + '\n```\n') + + manual_commands = os.path.join(target.scandir, '_manual_commands.txt') + if os.path.isfile(manual_commands): + with open(os.path.join(report, target.address, 'Manual Commands' + '.md'), 'w') as output: + with open(manual_commands, 'r') as file: + output.writelines('```bash\n' + file.read() + '\n```') + + patterns = os.path.join(target.scandir, '_patterns.log') + if os.path.isfile(patterns): + with open(os.path.join(report, target.address, 'Patterns' + '.md'), 'w') as output: + with open(patterns, 'r') as file: + output.writelines(file.read()) + + commands = os.path.join(target.scandir, '_commands.log') + if os.path.isfile(commands): + with open(os.path.join(report, target.address, 'Commands' + '.md'), 'w') as output: + with open(commands, 'r') as file: + output.writelines('```bash\n' + file.read() + '\n```') + + errors = os.path.join(target.scandir, '_errors.log') + if os.path.isfile(errors): + with open(os.path.join(report, target.address, 'Errors' + '.md'), 'w') as output: + with open(errors, 'r') as file: + output.writelines('```\n' + file.read() + '\n```') From 847bb67743759f96ff565c562912809b9a61e529 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Thu, 9 Sep 2021 12:39:06 -0400 Subject: [PATCH 81/95] Update autorecon.py Made secure/insecure optional for --force-services Fixed bug with keyboard monitor. --- autorecon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autorecon.py b/autorecon.py index 2e35c99..219b022 100644 --- a/autorecon.py +++ b/autorecon.py @@ -390,7 +390,7 @@ async def scan_target(target): forced_services = [x.strip().lower() for x in config['force_services']] for forced_service in forced_services: - match = re.search('(?P(tcp|udp))\/(?P\d+)\/(?P[\w\-\/]+)\/(?Psecure|insecure)', forced_service) + match = re.search('(?P(tcp|udp))\/(?P\d+)\/(?P[\w\-]+)(\/(?Psecure|insecure))?', forced_service) if match: protocol = match.group('protocol') if config['proxychains'] and protocol == 'udp': @@ -408,7 +408,6 @@ async def scan_target(target): else: error('No services were defined. Please check your service syntax: [tcp|udp]///[secure|insecure]') heartbeat.cancel() - keyboard_monitor.cancel() autorecon.errors = True return else: @@ -1295,7 +1294,8 @@ async def main(): if i >= num_new_targets: break - keyboard_monitor.cancel() + if not config['disable_keyboard_control']: + keyboard_monitor.cancel() # If there's only one target we don't need a combined report if len(autorecon.completed_targets) > 1: From 520cd9c91faa016cdab4882cd8b3fdd52d3f2ecf Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Thu, 9 Sep 2021 20:04:30 -0400 Subject: [PATCH 82/95] Added tag override options for all three types of plugins. --- autorecon.py | 114 +++++++++++++++++++++++++++----------------- autorecon/config.py | 6 +++ 2 files changed, 77 insertions(+), 43 deletions(-) diff --git a/autorecon.py b/autorecon.py index 219b022..5edeff8 100644 --- a/autorecon.py +++ b/autorecon.py @@ -415,19 +415,23 @@ async def scan_target(target): if config['proxychains'] and plugin.type == 'udp': continue - plugin_tag_set = set(plugin.tags) + if config['port_scans'] and plugin.slug in config['port_scans']: + matching_tags = True + excluded_tags = False + else: + plugin_tag_set = set(plugin.tags) - matching_tags = False - for tag_group in target.autorecon.tags: - if set(tag_group).issubset(plugin_tag_set): - matching_tags = True - break + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break - excluded_tags = False - for tag_group in target.autorecon.excluded_tags: - if set(tag_group).issubset(plugin_tag_set): - excluded_tags = True - break + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break if matching_tags and not excluded_tags: target.scans['ports'][plugin.slug] = {'plugin':plugin, 'commands':[]} @@ -539,19 +543,23 @@ async def scan_target(target): plugin_service_match = True if plugin_service_match: - plugin_tag_set = set(plugin.tags) + if config['service_scans'] and plugin.slug in config['service_scans']: + matching_tags = True + excluded_tags = False + else: + plugin_tag_set = set(plugin.tags) - matching_tags = False - for tag_group in target.autorecon.tags: - if set(tag_group).issubset(plugin_tag_set): - matching_tags = True - break + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break - excluded_tags = False - for tag_group in target.autorecon.excluded_tags: - if set(tag_group).issubset(plugin_tag_set): - excluded_tags = True - break + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break # TODO: Maybe make this less messy, keep manual-only plugins separate? plugin_is_runnable = False @@ -656,19 +664,23 @@ async def scan_target(target): target.autorecon.missing_services.append(service.full_tag()) for plugin in target.autorecon.plugin_types['report']: - plugin_tag_set = set(plugin.tags) + if config['reports'] and plugin.slug in config['reports']: + matching_tags = True + excluded_tags = False + else: + plugin_tag_set = set(plugin.tags) - matching_tags = False - for tag_group in target.autorecon.tags: - if set(tag_group).issubset(plugin_tag_set): - matching_tags = True - break + matching_tags = False + for tag_group in target.autorecon.tags: + if set(tag_group).issubset(plugin_tag_set): + matching_tags = True + break - excluded_tags = False - for tag_group in target.autorecon.excluded_tags: - if set(tag_group).issubset(plugin_tag_set): - excluded_tags = True - break + excluded_tags = False + for tag_group in target.autorecon.excluded_tags: + if set(tag_group).issubset(plugin_tag_set): + excluded_tags = True + break if matching_tags and not excluded_tags: pending.add(asyncio.create_task(generate_report(plugin, [target]))) @@ -710,6 +722,9 @@ async def main(): parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') + parser.add_argument('--port-scans', action='store', type=str, help='Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: %(default)s') + parser.add_argument('--service-scans', action='store', type=str, help='Override --tags / --exclude-tags for the listed ServiceScan plugins (comma separated). Default: %(default)s') + parser.add_argument('--reports', action='store', type=str, help='Override --tags / --exclude-tags for the listed Report plugins (comma separated). Default: %(default)s') parser.add_argument('--plugins-dir', action='store', type=str, help='The location of the plugins directory. Default: %(default)s') parser.add_argument('--add-plugins-dir', action='store', type=str, help='The location of an additional plugins directory to add to the main one. Default: %(default)s') parser.add_argument('-l', '--list', action='store', nargs='?', const='plugins', help='List all plugins or plugins of a specific type. e.g. --list, --list port, --list service') @@ -1090,6 +1105,15 @@ async def main(): # Remove duplicate lists from list. [autorecon.excluded_tags.append(t) for t in excluded_tags if t not in autorecon.excluded_tags] + if config['port_scans']: + config['port_scans'] = [x.strip().lower() for x in config['port_scans'].split(',')] + + if config['service_scans']: + config['service_scans'] = [x.strip().lower() for x in config['service_scans'].split(',')] + + if config['reports']: + config['reports'] = [x.strip().lower() for x in config['reports'].split(',')] + raw_targets = args.targets if len(args.target_file) > 0: @@ -1209,17 +1233,21 @@ async def main(): if not config['force_services']: port_scan_plugin_count = 0 for plugin in autorecon.plugin_types['port']: - matching_tags = False - for tag_group in autorecon.tags: - if set(tag_group).issubset(set(plugin.tags)): - matching_tags = True - break + if config['port_scans'] and plugin.slug in config['port_scans']: + matching_tags = True + excluded_tags = False + else: + matching_tags = False + for tag_group in autorecon.tags: + if set(tag_group).issubset(set(plugin.tags)): + matching_tags = True + break - excluded_tags = False - for tag_group in autorecon.excluded_tags: - if set(tag_group).issubset(set(plugin.tags)): - excluded_tags = True - break + excluded_tags = False + for tag_group in autorecon.excluded_tags: + if set(tag_group).issubset(set(plugin.tags)): + excluded_tags = True + break if matching_tags and not excluded_tags: port_scan_plugin_count += 1 diff --git a/autorecon/config.py b/autorecon/config.py index 26ec78f..d269aeb 100644 --- a/autorecon/config.py +++ b/autorecon/config.py @@ -6,6 +6,9 @@ configurable_keys = [ 'max_port_scans', 'tags', 'exclude_tags', + 'port_scans', + 'service_scans', + 'reports', 'plugins_dir', 'add_plugins-dir', 'outdir', @@ -42,6 +45,9 @@ config = { 'max_port_scans': None, 'tags': 'default', 'exclude_tags': None, + 'port_scans': None, + 'service_scans': None, + 'reports': None, 'plugins_dir': os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))) + '/plugins', 'add_plugins_dir': None, 'outdir': 'results', From 75c5ed7363e2590f62edea475e5cec613bb38604 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 11 Sep 2021 23:15:28 -0400 Subject: [PATCH 83/95] Added new verbose levels. Changed the TCP specific port scan to "All TCP Ports". --- autorecon.py | 40 ++++++++++++++++++------------------ autorecon/io.py | 15 +++++++------- autorecon/targets.py | 6 ++---- plugins/default-port-scan.py | 28 ++++++++++++------------- 4 files changed, 43 insertions(+), 46 deletions(-) diff --git a/autorecon.py b/autorecon.py index 5edeff8..da490d3 100644 --- a/autorecon.py +++ b/autorecon.py @@ -102,7 +102,7 @@ async def keyboard(): if len(input) >= 3: if input[:3] == '\x1b[A': input = '' - if config['verbose'] == 2: + if config['verbose'] == 3: info('Verbosity is already at the highest level.') else: config['verbose'] += 1 @@ -186,18 +186,18 @@ async def port_scan(plugin, target): if config['ports']['udp']: target.ports['udp'] = ','.join(config['ports']['udp']) if plugin.specific_ports is False: - warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} cannot be used to scan specific ports, and --ports was used. Skipping.') + warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} cannot be used to scan specific ports, and --ports was used. Skipping.', verbosity=2) return {'type':'port', 'plugin':plugin, 'result':[]} else: if plugin.type == 'tcp' and not config['ports']['tcp']: - warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping') + warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a TCP port scan but no TCP ports were set using --ports. Skipping', verbosity=2) return {'type':'port', 'plugin':plugin, 'result':[]} elif plugin.type == 'udp' and not config['ports']['udp']: - warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping') + warn('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} is a UDP port scan but no UDP ports were set using --ports. Skipping', verbosity=2) return {'type':'port', 'plugin':plugin, 'result':[]} async with target.autorecon.port_scan_semaphore: - info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}') + info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} running against {byellow}' + target.address + '{rst}', verbosity=1) start_time = time.time() @@ -213,7 +213,7 @@ async def port_scan(plugin, target): for process_dict in target.running_tasks[plugin.slug]['processes']: if process_dict['process'].returncode is None: - warn('A process was left running after port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + warn('A process was left running after port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.', verbosity=2) await process_dict['process'].wait() if process_dict['process'].returncode != 0: @@ -224,7 +224,7 @@ async def port_scan(plugin, target): errors.append(line + '\n') else: break - error('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.') + error('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} ran a command against {byellow}' + target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + target.scandir + '/_errors.log for more details.', verbosity=2) async with target.lock: with open(os.path.join(target.scandir, '_errors.log'), 'a') as file: file.writelines('[*] Port scan ' + plugin.name + ' (' + plugin.slug + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') @@ -239,7 +239,7 @@ async def port_scan(plugin, target): async with target.lock: target.running_tasks.pop(plugin.slug, None) - info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time) + info('Port scan {bblue}' + plugin.name + ' {green}(' + plugin.slug + '){rst} against {byellow}' + target.address + '{rst} finished in ' + elapsed_time, verbosity=2) return {'type':'port', 'plugin':plugin, 'result':result} async def service_scan(plugin, service): @@ -285,7 +285,7 @@ async def service_scan(plugin, service): tag = service.tag() + '/' + plugin.slug - info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}') + info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} running against {byellow}' + service.target.address + '{rst}', verbosity=1) start_time = time.time() @@ -301,7 +301,7 @@ async def service_scan(plugin, service): for process_dict in service.target.running_tasks[tag]['processes']: if process_dict['process'].returncode is None: - warn('A process was left running after service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.') + warn('A process was left running after service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished. Please ensure non-blocking processes are awaited before the run coroutine finishes. Awaiting now.', verbosity=2) await process_dict['process'].wait() if process_dict['process'].returncode != 0: @@ -312,7 +312,7 @@ async def service_scan(plugin, service): errors.append(line + '\n') else: break - error('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.') + error('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} ran a command against {byellow}' + service.target.address + '{rst} which returned a non-zero exit code (' + str(process_dict['process'].returncode) + '). Check ' + service.target.scandir + '/_errors.log for more details.', verbosity=2) async with service.target.lock: with open(os.path.join(service.target.scandir, '_errors.log'), 'a') as file: file.writelines('[*] Service scan ' + plugin.name + ' (' + tag + ') ran a command which returned a non-zero exit code (' + str(process_dict['process'].returncode) + ').\n') @@ -327,7 +327,7 @@ async def service_scan(plugin, service): async with service.target.lock: service.target.running_tasks.pop(tag, None) - info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time) + info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} against {byellow}' + service.target.address + '{rst} finished in ' + elapsed_time, verbosity=2) return {'type':'service', 'plugin':plugin, 'result':result} async def generate_report(plugin, targets): @@ -394,7 +394,7 @@ async def scan_target(target): if match: protocol = match.group('protocol') if config['proxychains'] and protocol == 'udp': - error('The service ' + forced_service + ' uses UDP and --proxychains is enabled. Skipping.') + error('The service ' + forced_service + ' uses UDP and --proxychains is enabled. Skipping.', verbosity=2) continue port = int(match.group('port')) service = match.group('service') @@ -481,7 +481,7 @@ async def scan_target(target): else: continue - info('Identified service {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}') + info('Identified service {bmagenta}' + service.name + '{rst} on {bmagenta}' + service.protocol + '/' + str(service.port) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) if not config['only_scans_dir']: with open(os.path.join(target.reportdir, 'notes.txt'), 'a') as file: @@ -575,7 +575,7 @@ async def scan_target(target): for s in target.scans['services']: if plugin.slug in target.scans['services'][s]: plugin_queued = True - warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin should only be run once and it appears to have already been queued. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin should only be run once and it appears to have already been queued. Skipping.{rst}', verbosity=2) break if plugin_queued: break @@ -588,18 +588,18 @@ async def scan_target(target): # Skip plugin if service port is in ignore_ports: if port in plugin.ignore_ports[protocol]: plugin_service_match = False - warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against ' + protocol + ' port ' + str(port) + '. Skipping.{rst}', verbosity=2) break # Skip plugin if plugin has required ports and service port is not in them: if plugin.ports[protocol] and port not in plugin.ports[protocol]: plugin_service_match = False - warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin can only run on specific ports. Skipping.{rst}', verbosity=2) break for i in plugin.ignore_service_names: if re.search(i, service.name): - warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against this service. Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin cannot be run against this service. Skipping.{rst}', verbosity=2) break # TODO: check if plugin matches tags, BUT run manual commands anyway! @@ -646,7 +646,7 @@ async def scan_target(target): for s in target.scans['services']: if plugin_tag in target.scans['services'][s]: plugin_queued = True - warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}') + warn('{byellow}[' + plugin_tag + ' against ' + target.address + ']{srst} Plugin appears to have already been queued, but it is not marked as run_once. Possible duplicate service tag? Skipping.{rst}', verbosity=2) break if plugin_queued: @@ -659,7 +659,7 @@ async def scan_target(target): pending.add(asyncio.create_task(service_scan(plugin, service))) if not service_match: - warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins based on the service name.{rst}') + warn('{byellow}[' + target.address + ']{srst} Service ' + service.full_tag() + ' did not match any plugins based on the service name.{rst}', verbosity=2) if service.full_tag() not in target.autorecon.missing_services: target.autorecon.missing_services.append(service.full_tag()) diff --git a/autorecon/io.py b/autorecon/io.py index 5d0c574..a813a6a 100644 --- a/autorecon/io.py +++ b/autorecon/io.py @@ -19,7 +19,9 @@ def e(*args, frame_index=1, **kvargs): def fformat(s): return e(s, frame_index=3) -def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, **kvargs): +def cprint(*args, color=Fore.RESET, char='*', sep=' ', end='\n', frame_index=1, file=sys.stdout, printmsg=True, verbosity=0, **kvargs): + if printmsg and verbosity > config['verbose']: + return '' frame = sys._getframe(frame_index) vals = { @@ -119,9 +121,8 @@ class CommandStreamReader(object): error('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} A line was longer than 64 KiB and cannot be processed. Ignoring.') continue - if config['verbose'] >= 2: - if line != '': - info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} ' + line.replace('{', '{{').replace('}', '}}')) + if line != '': + info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} ' + line.replace('{', '{{').replace('}', '}}'), verbosity=3) # Check lines for pattern matches. for p in self.patterns: @@ -130,12 +131,10 @@ class CommandStreamReader(object): async with self.target.lock: with open(os.path.join(self.target.scandir, '_patterns.log'), 'a') as file: if p.description: - if config['verbose'] >= 1: - info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} {bmagenta}' + p.description.replace('{match}', match) + '{rst}') + info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} {bmagenta}' + p.description.replace('{match}', match) + '{rst}', verbosity=2) file.writelines(p.description.replace('{match}', match) + '\n\n') else: - if config['verbose'] >= 1: - info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} {bmagenta}Matched Pattern: ' + match + '{rst}') + info('{bright}[{yellow}' + self.target.address + '{crst}/{bgreen}' + self.tag + '{crst}]{rst} {bmagenta}Matched Pattern: ' + match + '{rst}', verbosity=2) file.writelines('Matched Pattern: ' + match + '\n\n') if self.outfile is not None: diff --git a/autorecon/targets.py b/autorecon/targets.py index 1cc03e7..014ef71 100644 --- a/autorecon/targets.py +++ b/autorecon/targets.py @@ -59,8 +59,7 @@ class Target: cmd = e(cmd) tag = plugin.slug - if config['verbose'] >= 1: - info('Port scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + info('Port scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd, verbosity=2) if outfile is not None: outfile = os.path.join(target.scandir, e(outfile)) @@ -166,8 +165,7 @@ class Service: if plugin.run_once_boolean: plugin_tag = plugin.slug - if config['verbose'] >= 1: - info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd) + info('Service scan {bblue}' + plugin.name + ' {green}(' + tag + '){rst} is running the following command against {byellow}' + address + '{rst}: ' + cmd, verbosity=2) if outfile is not None: outfile = os.path.join(scandir, e(outfile)) diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py index f9ff930..8a4036d 100644 --- a/plugins/default-port-scan.py +++ b/plugins/default-port-scan.py @@ -10,23 +10,19 @@ class QuickTCPPortScan(PortScan): self.name = 'Top TCP Ports' self.description = 'Performs an Nmap scan of the top 1000 TCP ports.' self.type = 'tcp' - self.specific_ports = True self.tags = ['default', 'default-port-scan'] self.priority = 0 async def run(self, target): + if target.ports: # Don't run this plugin if there are custom ports. + return [] + if config['proxychains']: traceroute_os = '' else: traceroute_os = ' -A --osscan-guess' - if target.ports: - if target.ports['tcp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) - else: - return [] - else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) services = await target.extract_services(stdout) await process.wait() return services @@ -38,25 +34,29 @@ class AllTCPPortScan(PortScan): self.name = 'All TCP Ports' self.description = 'Performs an Nmap scan of all TCP ports.' self.type = 'tcp' + self.specific_ports = True self.tags = ['default', 'default-port-scan', 'long'] async def run(self, target): - if target.ports: # Don't run this plugin if there are custom ports. - return [] - if config['proxychains']: traceroute_os = '' else: traceroute_os = ' -A --osscan-guess' - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + if target.ports: + if target.ports['tcp']: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p ' + target.ports['tcp'] + ' -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + else: + return [] + else: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) services = [] while True: line = await stdout.readline() if line is not None: match = re.search('^Discovered open port ([0-9]+)/tcp', line) if match: - info('Discovered open port {bmagenta}tcp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}') + info('Discovered open port {bmagenta}tcp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) service = target.extract_service(line) if service: services.append(service) @@ -91,7 +91,7 @@ class Top100UDPPortScan(PortScan): if line is not None: match = re.search('^Discovered open port ([0-9]+)/udp', line) if match: - info('Discovered open port {bmagenta}udp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}') + info('Discovered open port {bmagenta}udp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) service = target.extract_service(line) if service: services.append(service) From d7777adb333612de6172b3edafea74720af9b714 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sat, 11 Sep 2021 23:47:07 -0400 Subject: [PATCH 84/95] Updated documentation. --- README.md | 117 ++++++++++++++++++++++++++++++--------------------- autorecon.py | 14 +++--- 2 files changed, 75 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index a1aa5f7..b29e614 100644 --- a/README.md +++ b/README.md @@ -117,63 +117,79 @@ See detailed usage options below. AutoRecon uses Python 3 specific functionality and does not support Python 2. ``` -usage: autorecon.py [-t TARGET_FILE] [-p PORTS] [-m MAX_SCANS] [-mp MAX_PORT_SCANS] [-c CONFIG_FILE] [-g GLOBAL_FILE] [--tags TAGS] [--exclude-tags EXCLUDE_TAGS] - [--plugins-dir PLUGINS_DIR] [-o OUTDIR] [--single-target] [--only-scans-dir] [--create-port-dirs] [--heartbeat HEARTBEAT] [--timeout TIMEOUT] - [--target-timeout TARGET_TIMEOUT] [--nmap NMAP | --nmap-append NMAP_APPEND] [--disable-sanity-checks] [--disable-keyboard-control] - [--force-services FORCE_SERVICES [FORCE_SERVICES ...]] [--accessible] [-v] [--version] [--curl.path VALUE] - [--dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb}] [--dirbuster.wordlist VALUE [VALUE ...]] [--dirbuster.threads VALUE] - [--dirbuster.ext VALUE] [--onesixtyone.community-strings VALUE] [--global.username-wordlist VALUE] [--global.password-wordlist VALUE] - [--global.domain VALUE] [-h] - [targets ...] - -Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets. - -positional arguments: - targets IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan. - -optional arguments: - -t TARGET_FILE, --targets TARGET_FILE - Read targets from file. - -p PORTS, --ports PORTS - Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both TCP/UDP, put port(s) at start - or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: None - -m MAX_SCANS, --max-scans MAX_SCANS - The maximum number of concurrent scans to run. Default: 50 - -mp MAX_PORT_SCANS, --max-port-scans MAX_PORT_SCANS - The maximum number of concurrent port scans to run. Default: 10 (approx 20% of max-scans unless specified) - -c CONFIG_FILE, --config CONFIG_FILE - Location of AutoRecon's config file. Default: /mnt/hgfs/AutoRecon/config.toml - -g GLOBAL_FILE, --global-file GLOBAL_FILE - Location of AutoRecon's global file. Default: /mnt/hgfs/AutoRecon/global.toml - --tags TAGS Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to - create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: default - --exclude-tags EXCLUDE_TAGS - Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to - create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: None - --plugins-dir PLUGINS_DIR +usage: autorecon.py [-t TARGET_FILE] [-p PORTS] [-m MAX_SCANS] [-mp MAX_PORT_SCANS] [-c CONFIG_FILE] [-g GLOBAL_FILE] [--tags TAGS] + [--exclude-tags TAGS] [--port-scans PLUGINS] [--service-scans PLUGINS] [--reports PLUGINS] [--plugins-dir PLUGINS_DIR] + [--add-plugins-dir PLUGINS_DIR] [-l [TYPE]] [-o OUTDIR] [--single-target] [--only-scans-dir] [--create-port-dirs] + [--heartbeat HEARTBEAT] [--timeout TIMEOUT] [--target-timeout TARGET_TIMEOUT] [--nmap NMAP | --nmap-append NMAP_APPEND] + [--proxychains] [--disable-sanity-checks] [--disable-keyboard-control] [--force-services SERVICE [SERVICE ...]] + [--accessible] [-v] [--version] [--curl.path VALUE] [--dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb}] + [--dirbuster.wordlist VALUE [VALUE ...]] [--dirbuster.threads VALUE] [--dirbuster.ext VALUE] + [--onesixtyone.community-strings VALUE] [--global.username-wordlist VALUE] [--global.password-wordlist VALUE] + [--global.domain VALUE] [-h] + [targets ...] + +Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets. + +positional arguments: + targets IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan. + +optional arguments: + -t TARGET_FILE, --targets TARGET_FILE + Read targets from file. + -p PORTS, --ports PORTS + Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both + TCP/UDP, put port(s) at start or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: None + -m MAX_SCANS, --max-scans MAX_SCANS + The maximum number of concurrent scans to run. Default: 50 + -mp MAX_PORT_SCANS, --max-port-scans MAX_PORT_SCANS + The maximum number of concurrent port scans to run. Default: 10 (approx 20% of max-scans unless specified) + -c CONFIG_FILE, --config CONFIG_FILE + Location of AutoRecon's config file. Default: /mnt/hgfs/AutoRecon/config.toml + -g GLOBAL_FILE, --global-file GLOBAL_FILE + Location of AutoRecon's global file. Default: /mnt/hgfs/AutoRecon/global.toml + --tags TAGS Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate + groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in + at least one group. Default: default + --exclude-tags TAGS Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate + groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in + at least one group. Default: None + --port-scans PLUGINS Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: None + --service-scans PLUGINS + Override --tags / --exclude-tags for the listed ServiceScan plugins (comma separated). Default: None + --reports PLUGINS Override --tags / --exclude-tags for the listed Report plugins (comma separated). Default: None + --plugins-dir PLUGINS_DIR The location of the plugins directory. Default: /mnt/hgfs/AutoRecon/plugins + --add-plugins-dir PLUGINS_DIR + The location of an additional plugins directory to add to the main one. Default: None + -l [TYPE], --list [TYPE] + List all plugins or plugins of a specific type. e.g. --list, --list port, --list service -o OUTDIR, --output OUTDIR The output directory for results. Default: results - --single-target Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the - output directory. Default: False - --only-scans-dir Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: False - --create-port-dirs Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and store results in these directories. Default: False + --single-target Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will + be created within the output directory. Default: False + --only-scans-dir Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. + Default: False + --create-port-dirs Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and store results in these + directories. Default: True --heartbeat HEARTBEAT Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60 --timeout TIMEOUT Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: None --target-timeout TARGET_TIMEOUT - Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. Default: None + Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. + Default: None --nmap NMAP Override the {nmap_extra} variable in scans. Default: -vv --reason -Pn --nmap-append NMAP_APPEND - Append to the default {nmap_extra} variable in scans. Default: + Append to the default {nmap_extra} variable in scans. Default: -T4 + --proxychains Use if you are running AutoRecon via proxychains. Default: False --disable-sanity-checks Disable sanity checks that would otherwise prevent the scans from running. Default: False --disable-keyboard-control Disables keyboard control ([s]tatus, Up, Down) if you are in SSH or Docker. - --force-services FORCE_SERVICES [FORCE_SERVICES ...] - A space separated list of services in the following style: tcp/80/http/insecure tcp/443/https/secure + --force-services SERVICE [SERVICE ...] + A space separated list of services in the following style: tcp/80/http tcp/443/https/secure --accessible Attempts to make AutoRecon output more accessible to screenreaders. Default: False -v, --verbose Enable verbose output. Repeat for more verbosity. + --version Prints the AutoRecon version and exits. -h, --help Show this help message and exit. plugin arguments: @@ -183,14 +199,16 @@ plugin arguments: --dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb} The tool to use for directory busting. Default: feroxbuster --dirbuster.wordlist VALUE [VALUE ...] - The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: ['/usr/share/seclists/Discovery/Web- - Content/common.txt'] + The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: + ['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web-Content/big.txt', + '/usr/share/seclists/Discovery/Web-Content/raft-large-words.txt'] --dirbuster.threads VALUE The number of threads to use when directory busting. Default: 10 --dirbuster.ext VALUE The extensions you wish to fuzz (no dot, comma separated). Default: txt,html,php,asp,aspx,jsp --onesixtyone.community-strings VALUE - The file containing a list of community strings to try. Default: /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt + The file containing a list of community strings to try. Default: /usr/share/seclists/Discovery/SNMP/common-snmp- + community-strings-onesixtyone.txt global plugin arguments: These are optional arguments that can be used by all plugins. @@ -205,11 +223,12 @@ global plugin arguments: ### Verbosity -AutoRecon supports three levels of verbosity: +AutoRecon supports four levels of verbosity: -* (none) Minimal output. AutoRecon will announce when target scans start and finish, as well as which services were identified. -* (-v) Verbose output. AutoRecon will additionally specify the exact commands which are being run, as well as highlighting any patterns which are matched in command output. -* (-vv) Very verbose output. AutoRecon will output everything. Literally every line from all commands which are currently running. When scanning multiple targets concurrently, this can lead to a ridiculous amount of output. It is not advised to use -vv unless you absolutely need to see live output from commands. +* (none) Minimal output. AutoRecon will announce when scanning targets starts / ends. +* (-v) Verbose output. AutoRecon will additionally announce when plugins start running, and report open ports and identified services. +* (-vv) Very verbose output. AutoRecon will additionally specify the exact commands which are being run by plugins, highlight any patterns which are matched in command output, and announce when plugins end. +* (-vvv) Very very verbose output. AutoRecon will output everything. Literally every line from all commands which are currently running. When scanning multiple targets concurrently, this can lead to a ridiculous amount of output. It is not advised to use -vvv unless you absolutely need to see live output from commands. Note: You can change the verbosity of AutoRecon mid-scan by pressing the up and down arrow keys. diff --git a/autorecon.py b/autorecon.py index da490d3..950bb08 100644 --- a/autorecon.py +++ b/autorecon.py @@ -721,13 +721,13 @@ async def main(): parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') - parser.add_argument('--exclude-tags', action='store', type=str, default='', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') - parser.add_argument('--port-scans', action='store', type=str, help='Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: %(default)s') - parser.add_argument('--service-scans', action='store', type=str, help='Override --tags / --exclude-tags for the listed ServiceScan plugins (comma separated). Default: %(default)s') - parser.add_argument('--reports', action='store', type=str, help='Override --tags / --exclude-tags for the listed Report plugins (comma separated). Default: %(default)s') + parser.add_argument('--exclude-tags', action='store', type=str, default='', metavar='TAGS', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') + parser.add_argument('--port-scans', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: %(default)s') + parser.add_argument('--service-scans', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed ServiceScan plugins (comma separated). Default: %(default)s') + parser.add_argument('--reports', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed Report plugins (comma separated). Default: %(default)s') parser.add_argument('--plugins-dir', action='store', type=str, help='The location of the plugins directory. Default: %(default)s') - parser.add_argument('--add-plugins-dir', action='store', type=str, help='The location of an additional plugins directory to add to the main one. Default: %(default)s') - parser.add_argument('-l', '--list', action='store', nargs='?', const='plugins', help='List all plugins or plugins of a specific type. e.g. --list, --list port, --list service') + parser.add_argument('--add-plugins-dir', action='store', type=str, metavar='PLUGINS_DIR', help='The location of an additional plugins directory to add to the main one. Default: %(default)s') + parser.add_argument('-l', '--list', action='store', nargs='?', const='plugins', metavar='TYPE', help='List all plugins or plugins of a specific type. e.g. --list, --list port, --list service') parser.add_argument('-o', '--output', action='store', dest='outdir', help='The output directory for results. Default: %(default)s') parser.add_argument('--single-target', action='store_true', help='Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will be created within the output directory. Default: %(default)s') parser.add_argument('--only-scans-dir', action='store_true', help='Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. Default: %(default)s') @@ -741,7 +741,7 @@ async def main(): parser.add_argument('--proxychains', action='store_true', help='Use if you are running AutoRecon via proxychains. Default: %(default)s') parser.add_argument('--disable-sanity-checks', action='store_true', help='Disable sanity checks that would otherwise prevent the scans from running. Default: %(default)s') parser.add_argument('--disable-keyboard-control', action='store_true', help='Disables keyboard control ([s]tatus, Up, Down) if you are in SSH or Docker.') - parser.add_argument('--force-services', action='store', nargs='+', help='A space separated list of services in the following style: tcp/80/http/insecure tcp/443/https/secure') + parser.add_argument('--force-services', action='store', nargs='+', metavar='SERVICE', help='A space separated list of services in the following style: tcp/80/http tcp/443/https/secure') parser.add_argument('--accessible', action='store_true', help='Attempts to make AutoRecon output more accessible to screenreaders. Default: %(default)s') parser.add_argument('-v', '--verbose', action='count', help='Enable verbose output. Repeat for more verbosity.') parser.add_argument('--version', action='store_true', help='Prints the AutoRecon version and exits.') From 6ffa85b70a86c45d27f017cfc13b064ebeb54ce4 Mon Sep 17 00:00:00 2001 From: Greg Poisson <5056836+sordidlist@users.noreply.github.com> Date: Sun, 12 Sep 2021 10:36:25 -0600 Subject: [PATCH 85/95] add irc plugin (#105) * add irc plugin * move irc plugin to misc * remove irc.py, moved to misc --- plugins/misc.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/misc.py b/plugins/misc.py index 20d3615..4775909 100644 --- a/plugins/misc.py +++ b/plugins/misc.py @@ -66,6 +66,19 @@ class NmapIMAP(ServiceScan): async def run(self, service): await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}') +class NmapIrc(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap IRC' + self.tags = ['default', 'safe', 'irc'] + + def configure(self): + self.match_service_name('^irc') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV --script irc-botnet-channels,irc-info,irc-unrealircd-backdoor -oN "{scandir}/{protocol}_{port}_irc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_irc_nmap.xml" -p {port} {address}') + class NmapNNTP(ServiceScan): def __init__(self): From e7b147d7d735934760390f354abf0e588c942c70 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 12 Sep 2021 12:40:35 -0400 Subject: [PATCH 86/95] Reformatted code to be pip compatible. --- autorecon/{ => autorecon}/__init__.py | 0 autorecon/{ => autorecon}/config.py | 9 ++-- autorecon/{ => autorecon}/io.py | 0 autorecon/{ => autorecon}/plugins.py | 0 autorecon/{ => autorecon}/targets.py | 0 config.toml => autorecon/config.toml | 0 global.toml => autorecon/global.toml | 0 autorecon.py => autorecon/main.py | 27 +++++++--- {plugins => autorecon/plugins}/__init__.py | 0 {plugins => autorecon/plugins}/databases.py | 0 .../plugins}/default-port-scan.py | 0 {plugins => autorecon/plugins}/dns.py | 0 {plugins => autorecon/plugins}/ftp.py | 0 .../plugins}/guess-port-scan.py | 0 {plugins => autorecon/plugins}/http_server.py | 0 {plugins => autorecon/plugins}/kerberos.py | 0 {plugins => autorecon/plugins}/ldap.py | 0 {plugins => autorecon/plugins}/misc.py | 0 {plugins => autorecon/plugins}/nfs.py | 0 {plugins => autorecon/plugins}/rdp.py | 0 {plugins => autorecon/plugins}/redis.py | 0 {plugins => autorecon/plugins}/reporting.py | 0 {plugins => autorecon/plugins}/rpc.py | 0 {plugins => autorecon/plugins}/rsync.py | 0 {plugins => autorecon/plugins}/sip.py | 0 {plugins => autorecon/plugins}/smb.py | 0 {plugins => autorecon/plugins}/smtp.py | 0 {plugins => autorecon/plugins}/snmp.py | 0 {plugins => autorecon/plugins}/ssh.py | 0 {plugins => autorecon/plugins}/sslscan.py | 0 poetry.lock | 54 +++++++++++++++++++ pyproject.toml | 26 +++++++++ 32 files changed, 106 insertions(+), 10 deletions(-) rename autorecon/{ => autorecon}/__init__.py (100%) rename autorecon/{ => autorecon}/config.py (86%) rename autorecon/{ => autorecon}/io.py (100%) rename autorecon/{ => autorecon}/plugins.py (100%) rename autorecon/{ => autorecon}/targets.py (100%) rename config.toml => autorecon/config.toml (100%) rename global.toml => autorecon/global.toml (100%) rename autorecon.py => autorecon/main.py (98%) rename {plugins => autorecon/plugins}/__init__.py (100%) rename {plugins => autorecon/plugins}/databases.py (100%) rename {plugins => autorecon/plugins}/default-port-scan.py (100%) rename {plugins => autorecon/plugins}/dns.py (100%) rename {plugins => autorecon/plugins}/ftp.py (100%) rename {plugins => autorecon/plugins}/guess-port-scan.py (100%) rename {plugins => autorecon/plugins}/http_server.py (100%) rename {plugins => autorecon/plugins}/kerberos.py (100%) rename {plugins => autorecon/plugins}/ldap.py (100%) rename {plugins => autorecon/plugins}/misc.py (100%) rename {plugins => autorecon/plugins}/nfs.py (100%) rename {plugins => autorecon/plugins}/rdp.py (100%) rename {plugins => autorecon/plugins}/redis.py (100%) rename {plugins => autorecon/plugins}/reporting.py (100%) rename {plugins => autorecon/plugins}/rpc.py (100%) rename {plugins => autorecon/plugins}/rsync.py (100%) rename {plugins => autorecon/plugins}/sip.py (100%) rename {plugins => autorecon/plugins}/smb.py (100%) rename {plugins => autorecon/plugins}/smtp.py (100%) rename {plugins => autorecon/plugins}/snmp.py (100%) rename {plugins => autorecon/plugins}/ssh.py (100%) rename {plugins => autorecon/plugins}/sslscan.py (100%) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/autorecon/__init__.py b/autorecon/autorecon/__init__.py similarity index 100% rename from autorecon/__init__.py rename to autorecon/autorecon/__init__.py diff --git a/autorecon/config.py b/autorecon/autorecon/config.py similarity index 86% rename from autorecon/config.py rename to autorecon/autorecon/config.py index d269aeb..0279d63 100644 --- a/autorecon/config.py +++ b/autorecon/autorecon/config.py @@ -1,4 +1,6 @@ -import os +import appdirs, os + +config_dir = appdirs.user_config_dir('AutoRecon') configurable_keys = [ 'ports', @@ -39,7 +41,8 @@ configurable_boolean_keys = [ config = { 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], - 'global_file': os.path.dirname(os.path.realpath(os.path.join(__file__, '..'))) + '/global.toml', + 'config_dir': config_dir, + 'global_file': os.path.join(config_dir, 'global.toml'), 'ports': None, 'max_scans': 50, 'max_port_scans': None, @@ -48,7 +51,7 @@ config = { 'port_scans': None, 'service_scans': None, 'reports': None, - 'plugins_dir': os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))) + '/plugins', + 'plugins_dir': os.path.join(config_dir, 'plugins'), 'add_plugins_dir': None, 'outdir': 'results', 'single_target': False, diff --git a/autorecon/io.py b/autorecon/autorecon/io.py similarity index 100% rename from autorecon/io.py rename to autorecon/autorecon/io.py diff --git a/autorecon/plugins.py b/autorecon/autorecon/plugins.py similarity index 100% rename from autorecon/plugins.py rename to autorecon/autorecon/plugins.py diff --git a/autorecon/targets.py b/autorecon/autorecon/targets.py similarity index 100% rename from autorecon/targets.py rename to autorecon/autorecon/targets.py diff --git a/config.toml b/autorecon/config.toml similarity index 100% rename from config.toml rename to autorecon/config.toml diff --git a/global.toml b/autorecon/global.toml similarity index 100% rename from global.toml rename to autorecon/global.toml diff --git a/autorecon.py b/autorecon/main.py similarity index 98% rename from autorecon.py rename to autorecon/main.py index 950bb08..55afc6b 100644 --- a/autorecon.py +++ b/autorecon/main.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -import argparse, asyncio, importlib, inspect, ipaddress, math, os, re, sys, signal, select, socket, termios, time, traceback, tty +import appdirs, argparse, asyncio, importlib, inspect, ipaddress, math, os, re, select, shutil, signal, socket, sys, termios, time, traceback, tty from datetime import datetime try: @@ -17,6 +17,16 @@ from autorecon.io import slugify, e, fformat, cprint, debug, info, warn, error, from autorecon.plugins import Pattern, PortScan, ServiceScan, Report, AutoRecon from autorecon.targets import Target, Service +def install(): + shutil.rmtree(config['config_dir'], ignore_errors=True) + os.makedirs(config['config_dir'], exist_ok=True) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) + shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'plugins'), os.path.join(config['config_dir'], 'plugins')) + +if not os.path.exists(config['config_dir']): + install() + # Save current terminal settings so we can restore them. terminal_settings = termios.tcgetattr(sys.stdin.fileno()) @@ -711,15 +721,15 @@ async def scan_target(target): autorecon.completed_targets.append(target) autorecon.scanning_targets.remove(target) -async def main(): +async def run(): parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') parser.add_argument('-p', '--ports', action='store', type=str, help='Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both TCP/UDP, put port(s) at start or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: %(default)s') parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: %(default)s') parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') - parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') - parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') + parser.add_argument('-c', '--config', action='store', type=str, default=os.path.join(config['config_dir'], 'config.toml'), dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') + parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.join(config['config_dir'], 'global.toml')) parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--exclude-tags', action='store', type=str, default='', metavar='TAGS', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--port-scans', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: %(default)s') @@ -753,7 +763,7 @@ async def main(): autorecon.argparse = parser if args.version: - print('AutoRecon v2.0-beta3') + print('AutoRecon v2.0') sys.exit(0) # Parse config file and args for global.toml first. @@ -1368,12 +1378,15 @@ async def main(): # Restore original terminal settings. termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) -if __name__ == '__main__': +def main(): # Capture Ctrl+C and cancel everything. signal.signal(signal.SIGINT, cancel_all_tasks) try: - asyncio.run(main()) + asyncio.run(run()) except asyncio.exceptions.CancelledError: pass except RuntimeError: pass + +if __name__ == '__main__': + main() diff --git a/plugins/__init__.py b/autorecon/plugins/__init__.py similarity index 100% rename from plugins/__init__.py rename to autorecon/plugins/__init__.py diff --git a/plugins/databases.py b/autorecon/plugins/databases.py similarity index 100% rename from plugins/databases.py rename to autorecon/plugins/databases.py diff --git a/plugins/default-port-scan.py b/autorecon/plugins/default-port-scan.py similarity index 100% rename from plugins/default-port-scan.py rename to autorecon/plugins/default-port-scan.py diff --git a/plugins/dns.py b/autorecon/plugins/dns.py similarity index 100% rename from plugins/dns.py rename to autorecon/plugins/dns.py diff --git a/plugins/ftp.py b/autorecon/plugins/ftp.py similarity index 100% rename from plugins/ftp.py rename to autorecon/plugins/ftp.py diff --git a/plugins/guess-port-scan.py b/autorecon/plugins/guess-port-scan.py similarity index 100% rename from plugins/guess-port-scan.py rename to autorecon/plugins/guess-port-scan.py diff --git a/plugins/http_server.py b/autorecon/plugins/http_server.py similarity index 100% rename from plugins/http_server.py rename to autorecon/plugins/http_server.py diff --git a/plugins/kerberos.py b/autorecon/plugins/kerberos.py similarity index 100% rename from plugins/kerberos.py rename to autorecon/plugins/kerberos.py diff --git a/plugins/ldap.py b/autorecon/plugins/ldap.py similarity index 100% rename from plugins/ldap.py rename to autorecon/plugins/ldap.py diff --git a/plugins/misc.py b/autorecon/plugins/misc.py similarity index 100% rename from plugins/misc.py rename to autorecon/plugins/misc.py diff --git a/plugins/nfs.py b/autorecon/plugins/nfs.py similarity index 100% rename from plugins/nfs.py rename to autorecon/plugins/nfs.py diff --git a/plugins/rdp.py b/autorecon/plugins/rdp.py similarity index 100% rename from plugins/rdp.py rename to autorecon/plugins/rdp.py diff --git a/plugins/redis.py b/autorecon/plugins/redis.py similarity index 100% rename from plugins/redis.py rename to autorecon/plugins/redis.py diff --git a/plugins/reporting.py b/autorecon/plugins/reporting.py similarity index 100% rename from plugins/reporting.py rename to autorecon/plugins/reporting.py diff --git a/plugins/rpc.py b/autorecon/plugins/rpc.py similarity index 100% rename from plugins/rpc.py rename to autorecon/plugins/rpc.py diff --git a/plugins/rsync.py b/autorecon/plugins/rsync.py similarity index 100% rename from plugins/rsync.py rename to autorecon/plugins/rsync.py diff --git a/plugins/sip.py b/autorecon/plugins/sip.py similarity index 100% rename from plugins/sip.py rename to autorecon/plugins/sip.py diff --git a/plugins/smb.py b/autorecon/plugins/smb.py similarity index 100% rename from plugins/smb.py rename to autorecon/plugins/smb.py diff --git a/plugins/smtp.py b/autorecon/plugins/smtp.py similarity index 100% rename from plugins/smtp.py rename to autorecon/plugins/smtp.py diff --git a/plugins/snmp.py b/autorecon/plugins/snmp.py similarity index 100% rename from plugins/snmp.py rename to autorecon/plugins/snmp.py diff --git a/plugins/ssh.py b/autorecon/plugins/ssh.py similarity index 100% rename from plugins/ssh.py rename to autorecon/plugins/ssh.py diff --git a/plugins/sslscan.py b/autorecon/plugins/sslscan.py similarity index 100% rename from plugins/sslscan.py rename to autorecon/plugins/sslscan.py diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..eeaf603 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,54 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "unidecode" +version = "1.3.1" +description = "ASCII transliterations of Unicode text" +category = "main" +optional = false +python-versions = ">=3.5" + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "681db41aa556d6d3f79e1e8ee0107bccd078e39c8db7e6e0159860c96ea93c5b" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +unidecode = [ + {file = "Unidecode-1.3.1-py3-none-any.whl", hash = "sha256:5f58926b9125b499f8ab6816828e737578fa3e31fa24d351a3ab7f4b7c064ab0"}, + {file = "Unidecode-1.3.1.tar.gz", hash = "sha256:6efac090bf8f29970afc90caf4daae87b172709b786cb1b4da2d0c0624431ecc"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d8ba3c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "autorecon" +version = "2.0.0" +description = "A multi-threaded network reconaissance tool which performs automated enumeration of services." +authors = ["Tib3rius"] +license = "GNU GPL v3" +packages = [ + {include = "main.py", from = "autorecon"}, + {include = "autorecon", from = "autorecon"}, +] + +[tool.poetry.dependencies] +python = "^3.7" +appdirs = "^1.4.4" +colorama = "^0.4.4" +toml = "^0.10.2" +Unidecode = "^1.3.1" + +[tool.poetry.dev-dependencies] + +[tool.poetry.scripts] +autorecon = "main:main" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From a64a6d7cf799b20e7c4b7356ec0d8e8c489d2f91 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 12 Sep 2021 12:44:55 -0400 Subject: [PATCH 87/95] Create poetry.toml --- poetry.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 poetry.toml diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..3b549d6 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +create = true From fb60000b2805cfe8d1eb171fd2eb56a51f57d75b Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 12 Sep 2021 12:52:29 -0400 Subject: [PATCH 88/95] Potential fix for imports. --- autorecon/{ => config}/config.toml | 0 autorecon/{ => config}/global.toml | 0 autorecon/main.py | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename autorecon/{ => config}/config.toml (100%) rename autorecon/{ => config}/global.toml (100%) diff --git a/autorecon/config.toml b/autorecon/config/config.toml similarity index 100% rename from autorecon/config.toml rename to autorecon/config/config.toml diff --git a/autorecon/global.toml b/autorecon/config/global.toml similarity index 100% rename from autorecon/global.toml rename to autorecon/config/global.toml diff --git a/autorecon/main.py b/autorecon/main.py index 55afc6b..5ea88d8 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -20,8 +20,8 @@ from autorecon.targets import Target, Service def install(): shutil.rmtree(config['config_dir'], ignore_errors=True) os.makedirs(config['config_dir'], exist_ok=True) - shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) - shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config', 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config', 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'plugins'), os.path.join(config['config_dir'], 'plugins')) if not os.path.exists(config['config_dir']): From e551a86a710b031d24a6e8532ad80016e9041246 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 12 Sep 2021 13:04:04 -0400 Subject: [PATCH 89/95] Revert "Potential fix for imports." This reverts commit fb60000b2805cfe8d1eb171fd2eb56a51f57d75b. --- autorecon/{config => }/config.toml | 0 autorecon/{config => }/global.toml | 0 autorecon/main.py | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename autorecon/{config => }/config.toml (100%) rename autorecon/{config => }/global.toml (100%) diff --git a/autorecon/config/config.toml b/autorecon/config.toml similarity index 100% rename from autorecon/config/config.toml rename to autorecon/config.toml diff --git a/autorecon/config/global.toml b/autorecon/global.toml similarity index 100% rename from autorecon/config/global.toml rename to autorecon/global.toml diff --git a/autorecon/main.py b/autorecon/main.py index 5ea88d8..55afc6b 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -20,8 +20,8 @@ from autorecon.targets import Target, Service def install(): shutil.rmtree(config['config_dir'], ignore_errors=True) os.makedirs(config['config_dir'], exist_ok=True) - shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config', 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) - shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config', 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'plugins'), os.path.join(config['config_dir'], 'plugins')) if not os.path.exists(config['config_dir']): From 3de342c4cfd5ee8d874c949ba585ae1947410b7d Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Sun, 12 Sep 2021 13:05:27 -0400 Subject: [PATCH 90/95] Revert "Reformatted code to be pip compatible." This reverts commit e7b147d7d735934760390f354abf0e588c942c70. --- autorecon/main.py => autorecon.py | 27 +++------- autorecon/{autorecon => }/__init__.py | 0 autorecon/{autorecon => }/config.py | 9 ++-- autorecon/{autorecon => }/io.py | 0 autorecon/{autorecon => }/plugins.py | 0 autorecon/{autorecon => }/targets.py | 0 autorecon/config.toml => config.toml | 0 autorecon/global.toml => global.toml | 0 {autorecon/plugins => plugins}/__init__.py | 0 {autorecon/plugins => plugins}/databases.py | 0 .../plugins => plugins}/default-port-scan.py | 0 {autorecon/plugins => plugins}/dns.py | 0 {autorecon/plugins => plugins}/ftp.py | 0 .../plugins => plugins}/guess-port-scan.py | 0 {autorecon/plugins => plugins}/http_server.py | 0 {autorecon/plugins => plugins}/kerberos.py | 0 {autorecon/plugins => plugins}/ldap.py | 0 {autorecon/plugins => plugins}/misc.py | 0 {autorecon/plugins => plugins}/nfs.py | 0 {autorecon/plugins => plugins}/rdp.py | 0 {autorecon/plugins => plugins}/redis.py | 0 {autorecon/plugins => plugins}/reporting.py | 0 {autorecon/plugins => plugins}/rpc.py | 0 {autorecon/plugins => plugins}/rsync.py | 0 {autorecon/plugins => plugins}/sip.py | 0 {autorecon/plugins => plugins}/smb.py | 0 {autorecon/plugins => plugins}/smtp.py | 0 {autorecon/plugins => plugins}/snmp.py | 0 {autorecon/plugins => plugins}/ssh.py | 0 {autorecon/plugins => plugins}/sslscan.py | 0 poetry.lock | 54 ------------------- pyproject.toml | 26 --------- 32 files changed, 10 insertions(+), 106 deletions(-) rename autorecon/main.py => autorecon.py (98%) rename autorecon/{autorecon => }/__init__.py (100%) rename autorecon/{autorecon => }/config.py (86%) rename autorecon/{autorecon => }/io.py (100%) rename autorecon/{autorecon => }/plugins.py (100%) rename autorecon/{autorecon => }/targets.py (100%) rename autorecon/config.toml => config.toml (100%) rename autorecon/global.toml => global.toml (100%) rename {autorecon/plugins => plugins}/__init__.py (100%) rename {autorecon/plugins => plugins}/databases.py (100%) rename {autorecon/plugins => plugins}/default-port-scan.py (100%) rename {autorecon/plugins => plugins}/dns.py (100%) rename {autorecon/plugins => plugins}/ftp.py (100%) rename {autorecon/plugins => plugins}/guess-port-scan.py (100%) rename {autorecon/plugins => plugins}/http_server.py (100%) rename {autorecon/plugins => plugins}/kerberos.py (100%) rename {autorecon/plugins => plugins}/ldap.py (100%) rename {autorecon/plugins => plugins}/misc.py (100%) rename {autorecon/plugins => plugins}/nfs.py (100%) rename {autorecon/plugins => plugins}/rdp.py (100%) rename {autorecon/plugins => plugins}/redis.py (100%) rename {autorecon/plugins => plugins}/reporting.py (100%) rename {autorecon/plugins => plugins}/rpc.py (100%) rename {autorecon/plugins => plugins}/rsync.py (100%) rename {autorecon/plugins => plugins}/sip.py (100%) rename {autorecon/plugins => plugins}/smb.py (100%) rename {autorecon/plugins => plugins}/smtp.py (100%) rename {autorecon/plugins => plugins}/snmp.py (100%) rename {autorecon/plugins => plugins}/ssh.py (100%) rename {autorecon/plugins => plugins}/sslscan.py (100%) delete mode 100644 poetry.lock delete mode 100644 pyproject.toml diff --git a/autorecon/main.py b/autorecon.py similarity index 98% rename from autorecon/main.py rename to autorecon.py index 55afc6b..950bb08 100644 --- a/autorecon/main.py +++ b/autorecon.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -import appdirs, argparse, asyncio, importlib, inspect, ipaddress, math, os, re, select, shutil, signal, socket, sys, termios, time, traceback, tty +import argparse, asyncio, importlib, inspect, ipaddress, math, os, re, sys, signal, select, socket, termios, time, traceback, tty from datetime import datetime try: @@ -17,16 +17,6 @@ from autorecon.io import slugify, e, fformat, cprint, debug, info, warn, error, from autorecon.plugins import Pattern, PortScan, ServiceScan, Report, AutoRecon from autorecon.targets import Target, Service -def install(): - shutil.rmtree(config['config_dir'], ignore_errors=True) - os.makedirs(config['config_dir'], exist_ok=True) - shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) - shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) - shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'plugins'), os.path.join(config['config_dir'], 'plugins')) - -if not os.path.exists(config['config_dir']): - install() - # Save current terminal settings so we can restore them. terminal_settings = termios.tcgetattr(sys.stdin.fileno()) @@ -721,15 +711,15 @@ async def scan_target(target): autorecon.completed_targets.append(target) autorecon.scanning_targets.remove(target) -async def run(): +async def main(): parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') parser.add_argument('-p', '--ports', action='store', type=str, help='Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both TCP/UDP, put port(s) at start or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: %(default)s') parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: %(default)s') parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') - parser.add_argument('-c', '--config', action='store', type=str, default=os.path.join(config['config_dir'], 'config.toml'), dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') - parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.join(config['config_dir'], 'global.toml')) + parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') + parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--exclude-tags', action='store', type=str, default='', metavar='TAGS', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--port-scans', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: %(default)s') @@ -763,7 +753,7 @@ async def run(): autorecon.argparse = parser if args.version: - print('AutoRecon v2.0') + print('AutoRecon v2.0-beta3') sys.exit(0) # Parse config file and args for global.toml first. @@ -1378,15 +1368,12 @@ async def run(): # Restore original terminal settings. termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) -def main(): +if __name__ == '__main__': # Capture Ctrl+C and cancel everything. signal.signal(signal.SIGINT, cancel_all_tasks) try: - asyncio.run(run()) + asyncio.run(main()) except asyncio.exceptions.CancelledError: pass except RuntimeError: pass - -if __name__ == '__main__': - main() diff --git a/autorecon/autorecon/__init__.py b/autorecon/__init__.py similarity index 100% rename from autorecon/autorecon/__init__.py rename to autorecon/__init__.py diff --git a/autorecon/autorecon/config.py b/autorecon/config.py similarity index 86% rename from autorecon/autorecon/config.py rename to autorecon/config.py index 0279d63..d269aeb 100644 --- a/autorecon/autorecon/config.py +++ b/autorecon/config.py @@ -1,6 +1,4 @@ -import appdirs, os - -config_dir = appdirs.user_config_dir('AutoRecon') +import os configurable_keys = [ 'ports', @@ -41,8 +39,7 @@ configurable_boolean_keys = [ config = { 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], - 'config_dir': config_dir, - 'global_file': os.path.join(config_dir, 'global.toml'), + 'global_file': os.path.dirname(os.path.realpath(os.path.join(__file__, '..'))) + '/global.toml', 'ports': None, 'max_scans': 50, 'max_port_scans': None, @@ -51,7 +48,7 @@ config = { 'port_scans': None, 'service_scans': None, 'reports': None, - 'plugins_dir': os.path.join(config_dir, 'plugins'), + 'plugins_dir': os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))) + '/plugins', 'add_plugins_dir': None, 'outdir': 'results', 'single_target': False, diff --git a/autorecon/autorecon/io.py b/autorecon/io.py similarity index 100% rename from autorecon/autorecon/io.py rename to autorecon/io.py diff --git a/autorecon/autorecon/plugins.py b/autorecon/plugins.py similarity index 100% rename from autorecon/autorecon/plugins.py rename to autorecon/plugins.py diff --git a/autorecon/autorecon/targets.py b/autorecon/targets.py similarity index 100% rename from autorecon/autorecon/targets.py rename to autorecon/targets.py diff --git a/autorecon/config.toml b/config.toml similarity index 100% rename from autorecon/config.toml rename to config.toml diff --git a/autorecon/global.toml b/global.toml similarity index 100% rename from autorecon/global.toml rename to global.toml diff --git a/autorecon/plugins/__init__.py b/plugins/__init__.py similarity index 100% rename from autorecon/plugins/__init__.py rename to plugins/__init__.py diff --git a/autorecon/plugins/databases.py b/plugins/databases.py similarity index 100% rename from autorecon/plugins/databases.py rename to plugins/databases.py diff --git a/autorecon/plugins/default-port-scan.py b/plugins/default-port-scan.py similarity index 100% rename from autorecon/plugins/default-port-scan.py rename to plugins/default-port-scan.py diff --git a/autorecon/plugins/dns.py b/plugins/dns.py similarity index 100% rename from autorecon/plugins/dns.py rename to plugins/dns.py diff --git a/autorecon/plugins/ftp.py b/plugins/ftp.py similarity index 100% rename from autorecon/plugins/ftp.py rename to plugins/ftp.py diff --git a/autorecon/plugins/guess-port-scan.py b/plugins/guess-port-scan.py similarity index 100% rename from autorecon/plugins/guess-port-scan.py rename to plugins/guess-port-scan.py diff --git a/autorecon/plugins/http_server.py b/plugins/http_server.py similarity index 100% rename from autorecon/plugins/http_server.py rename to plugins/http_server.py diff --git a/autorecon/plugins/kerberos.py b/plugins/kerberos.py similarity index 100% rename from autorecon/plugins/kerberos.py rename to plugins/kerberos.py diff --git a/autorecon/plugins/ldap.py b/plugins/ldap.py similarity index 100% rename from autorecon/plugins/ldap.py rename to plugins/ldap.py diff --git a/autorecon/plugins/misc.py b/plugins/misc.py similarity index 100% rename from autorecon/plugins/misc.py rename to plugins/misc.py diff --git a/autorecon/plugins/nfs.py b/plugins/nfs.py similarity index 100% rename from autorecon/plugins/nfs.py rename to plugins/nfs.py diff --git a/autorecon/plugins/rdp.py b/plugins/rdp.py similarity index 100% rename from autorecon/plugins/rdp.py rename to plugins/rdp.py diff --git a/autorecon/plugins/redis.py b/plugins/redis.py similarity index 100% rename from autorecon/plugins/redis.py rename to plugins/redis.py diff --git a/autorecon/plugins/reporting.py b/plugins/reporting.py similarity index 100% rename from autorecon/plugins/reporting.py rename to plugins/reporting.py diff --git a/autorecon/plugins/rpc.py b/plugins/rpc.py similarity index 100% rename from autorecon/plugins/rpc.py rename to plugins/rpc.py diff --git a/autorecon/plugins/rsync.py b/plugins/rsync.py similarity index 100% rename from autorecon/plugins/rsync.py rename to plugins/rsync.py diff --git a/autorecon/plugins/sip.py b/plugins/sip.py similarity index 100% rename from autorecon/plugins/sip.py rename to plugins/sip.py diff --git a/autorecon/plugins/smb.py b/plugins/smb.py similarity index 100% rename from autorecon/plugins/smb.py rename to plugins/smb.py diff --git a/autorecon/plugins/smtp.py b/plugins/smtp.py similarity index 100% rename from autorecon/plugins/smtp.py rename to plugins/smtp.py diff --git a/autorecon/plugins/snmp.py b/plugins/snmp.py similarity index 100% rename from autorecon/plugins/snmp.py rename to plugins/snmp.py diff --git a/autorecon/plugins/ssh.py b/plugins/ssh.py similarity index 100% rename from autorecon/plugins/ssh.py rename to plugins/ssh.py diff --git a/autorecon/plugins/sslscan.py b/plugins/sslscan.py similarity index 100% rename from autorecon/plugins/sslscan.py rename to plugins/sslscan.py diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index eeaf603..0000000 --- a/poetry.lock +++ /dev/null @@ -1,54 +0,0 @@ -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "unidecode" -version = "1.3.1" -description = "ASCII transliterations of Unicode text" -category = "main" -optional = false -python-versions = ">=3.5" - -[metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "681db41aa556d6d3f79e1e8ee0107bccd078e39c8db7e6e0159860c96ea93c5b" - -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -unidecode = [ - {file = "Unidecode-1.3.1-py3-none-any.whl", hash = "sha256:5f58926b9125b499f8ab6816828e737578fa3e31fa24d351a3ab7f4b7c064ab0"}, - {file = "Unidecode-1.3.1.tar.gz", hash = "sha256:6efac090bf8f29970afc90caf4daae87b172709b786cb1b4da2d0c0624431ecc"}, -] diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index d8ba3c0..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,26 +0,0 @@ -[tool.poetry] -name = "autorecon" -version = "2.0.0" -description = "A multi-threaded network reconaissance tool which performs automated enumeration of services." -authors = ["Tib3rius"] -license = "GNU GPL v3" -packages = [ - {include = "main.py", from = "autorecon"}, - {include = "autorecon", from = "autorecon"}, -] - -[tool.poetry.dependencies] -python = "^3.7" -appdirs = "^1.4.4" -colorama = "^0.4.4" -toml = "^0.10.2" -Unidecode = "^1.3.1" - -[tool.poetry.dev-dependencies] - -[tool.poetry.scripts] -autorecon = "main:main" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" From cc0b70fd6b625c8dcbd4c097bbf206fe7db3e6c0 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 14 Sep 2021 15:50:12 -0400 Subject: [PATCH 91/95] Reformatted code to be pip compatible. --- autorecon/config.py | 9 +- autorecon/config.toml | 19 ++ autorecon/default-plugins/__init__.py | 0 autorecon/default-plugins/databases.py | 135 +++++++++++ .../default-plugins/default-port-scan.py | 103 +++++++++ autorecon/default-plugins/dns.py | 57 +++++ autorecon/default-plugins/ftp.py | 30 +++ autorecon/default-plugins/guess-port-scan.py | 48 ++++ autorecon/default-plugins/http_server.py | 214 ++++++++++++++++++ autorecon/default-plugins/kerberos.py | 17 ++ autorecon/default-plugins/ldap.py | 29 +++ autorecon/default-plugins/misc.py | 188 +++++++++++++++ autorecon/default-plugins/nfs.py | 40 ++++ autorecon/default-plugins/rdp.py | 30 +++ autorecon/default-plugins/redis.py | 37 +++ autorecon/default-plugins/reporting.py | 163 +++++++++++++ autorecon/default-plugins/rpc.py | 42 ++++ autorecon/default-plugins/rsync.py | 27 +++ autorecon/default-plugins/sip.py | 28 +++ autorecon/default-plugins/smb.py | 104 +++++++++ autorecon/default-plugins/smtp.py | 33 +++ autorecon/default-plugins/snmp.py | 53 +++++ autorecon/default-plugins/ssh.py | 30 +++ autorecon/default-plugins/sslscan.py | 16 ++ autorecon/global.toml | 22 ++ autorecon.py => autorecon/main.py | 29 ++- poetry.lock | 54 +++++ pyproject.toml | 22 ++ requirements.txt | 5 +- 29 files changed, 1571 insertions(+), 13 deletions(-) create mode 100644 autorecon/config.toml create mode 100644 autorecon/default-plugins/__init__.py create mode 100644 autorecon/default-plugins/databases.py create mode 100644 autorecon/default-plugins/default-port-scan.py create mode 100644 autorecon/default-plugins/dns.py create mode 100644 autorecon/default-plugins/ftp.py create mode 100644 autorecon/default-plugins/guess-port-scan.py create mode 100644 autorecon/default-plugins/http_server.py create mode 100644 autorecon/default-plugins/kerberos.py create mode 100644 autorecon/default-plugins/ldap.py create mode 100644 autorecon/default-plugins/misc.py create mode 100644 autorecon/default-plugins/nfs.py create mode 100644 autorecon/default-plugins/rdp.py create mode 100644 autorecon/default-plugins/redis.py create mode 100644 autorecon/default-plugins/reporting.py create mode 100644 autorecon/default-plugins/rpc.py create mode 100644 autorecon/default-plugins/rsync.py create mode 100644 autorecon/default-plugins/sip.py create mode 100644 autorecon/default-plugins/smb.py create mode 100644 autorecon/default-plugins/smtp.py create mode 100644 autorecon/default-plugins/snmp.py create mode 100644 autorecon/default-plugins/ssh.py create mode 100644 autorecon/default-plugins/sslscan.py create mode 100644 autorecon/global.toml rename autorecon.py => autorecon/main.py (98%) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/autorecon/config.py b/autorecon/config.py index d269aeb..0279d63 100644 --- a/autorecon/config.py +++ b/autorecon/config.py @@ -1,4 +1,6 @@ -import os +import appdirs, os + +config_dir = appdirs.user_config_dir('AutoRecon') configurable_keys = [ 'ports', @@ -39,7 +41,8 @@ configurable_boolean_keys = [ config = { 'protected_classes': ['autorecon', 'target', 'service', 'commandstreamreader', 'plugin', 'portscan', 'servicescan', 'global', 'pattern'], - 'global_file': os.path.dirname(os.path.realpath(os.path.join(__file__, '..'))) + '/global.toml', + 'config_dir': config_dir, + 'global_file': os.path.join(config_dir, 'global.toml'), 'ports': None, 'max_scans': 50, 'max_port_scans': None, @@ -48,7 +51,7 @@ config = { 'port_scans': None, 'service_scans': None, 'reports': None, - 'plugins_dir': os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))) + '/plugins', + 'plugins_dir': os.path.join(config_dir, 'plugins'), 'add_plugins_dir': None, 'outdir': 'results', 'single_target': False, diff --git a/autorecon/config.toml b/autorecon/config.toml new file mode 100644 index 0000000..a9df31d --- /dev/null +++ b/autorecon/config.toml @@ -0,0 +1,19 @@ +# Configure regular AutoRecon options at the top of this file. + +create-port-dirs = true +nmap-append = '-T4' +# verbose = 1 +# max-scans = 30 + +# Configure global options here. +# [global] +# username-wordlist = '/usr/share/seclists/Usernames/cirt-default-usernames.txt' + +# Configure plugin options here. +# [dirbuster] +# threads = 50 +# wordlist = [ +# '/usr/share/seclists/Discovery/Web-Content/common.txt', +# '/usr/share/seclists/Discovery/Web-Content/big.txt', +# '/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt' +# ] diff --git a/autorecon/default-plugins/__init__.py b/autorecon/default-plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autorecon/default-plugins/databases.py b/autorecon/default-plugins/databases.py new file mode 100644 index 0000000..4e5286b --- /dev/null +++ b/autorecon/default-plugins/databases.py @@ -0,0 +1,135 @@ +from autorecon.plugins import ServiceScan +from autorecon.io import error +from shutil import which + +class NmapMongoDB(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MongoDB" + self.tags = ['default', 'safe', 'databases'] + + def configure(self): + self.match_service_name('^mongod') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mongodb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mongodb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mongodb_nmap.xml" {address}') + +class NmapMSSQL(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MSSQL" + self.tags = ['default', 'safe', 'databases'] + + def configure(self): + self.match_service_name(['^mssql', '^ms\-sql']) + + def manual(self, service, plugin_was_run): + if service.target.ipversion == 'IPv4': + service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}') + +class NmapMYSQL(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MYSQL" + self.tags = ['default', 'safe', 'databases'] + + def configure(self): + self.match_service_name('^mysql') + + def manual(self, service, plugin_was_run): + if service.target.ipversion == 'IPv4': + service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}') + +class NmapOracle(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Oracle" + self.tags = ['default', 'safe', 'databases'] + + def configure(self): + self.match_service_name('^oracle') + + def manual(self, service, plugin_was_run): + service.add_manual_command('Brute-force SIDs using Nmap:', 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}') + +class OracleTNScmd(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle TNScmd" + self.tags = ['default', 'safe', 'databases'] + + def configure(self): + self.match_service_name('^oracle') + + def check(self): + if which('tnscmd10g') is None: + error('The tnscmd10g program could not be found. Make sure it is installed. (On Kali, run: sudo apt install tnscmd10g)') + + async def run(self, service): + if service.target.ipversion == 'IPv4': + await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') + await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') + +class OracleScanner(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle Scanner" + self.tags = ['default', 'safe', 'databases'] + + def configure(self): + self.match_service_name('^oracle') + + def check(self): + if which('oscanner') is None: + error('The oscanner program could not be found. Make sure it is installed. (On Kali, run: sudo apt install oscanner)') + + async def run(self, service): + await service.execute('oscanner -v -s {address} -P {port} 2>&1', outfile='{protocol}_{port}_oracle_scanner.txt') + +class OracleODAT(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle ODAT" + self.tags = ['default', 'safe', 'databases'] + + def configure(self): + self.match_service_name('^oracle') + + def manual(self, service, plugin_was_run): + service.add_manual_commands('Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:', [ + 'python odat.py tnscmd -s {address} -p {port} --ping', + 'python odat.py tnscmd -s {address} -p {port} --version', + 'python odat.py tnscmd -s {address} -p {port} --status', + 'python odat.py sidguesser -s {address} -p {port}', + 'python odat.py passwordguesser -s {address} -p {port} -d --accounts-file accounts/accounts_multiple.txt', + 'python odat.py tnspoison -s {address} -p {port} -d --test-module' + ]) + +class OraclePatator(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Oracle Patator" + self.tags = ['default', 'databases'] + + def configure(self): + self.match_service_name('^oracle') + + def manual(self, service, plugin_was_run): + service.add_manual_command('Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:', 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000') diff --git a/autorecon/default-plugins/default-port-scan.py b/autorecon/default-plugins/default-port-scan.py new file mode 100644 index 0000000..8a4036d --- /dev/null +++ b/autorecon/default-plugins/default-port-scan.py @@ -0,0 +1,103 @@ +from autorecon.plugins import PortScan +from autorecon.io import info, error +from autorecon.config import config +import os, re + +class QuickTCPPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = 'Top TCP Ports' + self.description = 'Performs an Nmap scan of the top 1000 TCP ports.' + self.type = 'tcp' + self.tags = ['default', 'default-port-scan'] + self.priority = 0 + + async def run(self, target): + if target.ports: # Don't run this plugin if there are custom ports. + return [] + + if config['proxychains']: + traceroute_os = '' + else: + traceroute_os = ' -A --osscan-guess' + + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + services = await target.extract_services(stdout) + await process.wait() + return services + +class AllTCPPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = 'All TCP Ports' + self.description = 'Performs an Nmap scan of all TCP ports.' + self.type = 'tcp' + self.specific_ports = True + self.tags = ['default', 'default-port-scan', 'long'] + + async def run(self, target): + if config['proxychains']: + traceroute_os = '' + else: + traceroute_os = ' -A --osscan-guess' + + if target.ports: + if target.ports['tcp']: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p ' + target.ports['tcp'] + ' -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + else: + return [] + else: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) + services = [] + while True: + line = await stdout.readline() + if line is not None: + match = re.search('^Discovered open port ([0-9]+)/tcp', line) + if match: + info('Discovered open port {bmagenta}tcp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) + service = target.extract_service(line) + if service: + services.append(service) + else: + break + await process.wait() + return services + +class Top100UDPPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = 'Top 100 UDP Ports' + self.description = 'Performs an Nmap scan of the top 100 UDP ports.' + self.type = 'udp' + self.specific_ports = True + self.tags = ['default', 'default-port-scan', 'long'] + + async def run(self, target): + # Only run UDP scan if user is root. + if os.getuid() == 0: + if target.ports: + if target.ports['udp']: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --osscan-guess -p ' + target.ports['udp'] + ' -oN "{scandir}/_custom_ports_udp_nmap.txt" -oX "{scandir}/xml/_custom_ports_udp_nmap.xml" {address}', blocking=False) + else: + return [] + else: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --top-ports 100 -oN "{scandir}/_top_100_udp_nmap.txt" -oX "{scandir}/xml/_top_100_udp_nmap.xml" {address}', blocking=False) + services = [] + while True: + line = await stdout.readline() + if line is not None: + match = re.search('^Discovered open port ([0-9]+)/udp', line) + if match: + info('Discovered open port {bmagenta}udp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) + service = target.extract_service(line) + if service: + services.append(service) + else: + break + await process.wait() + return services + else: + error('UDP scan requires AutoRecon be run with root privileges.') diff --git a/autorecon/default-plugins/dns.py b/autorecon/default-plugins/dns.py new file mode 100644 index 0000000..8793b3b --- /dev/null +++ b/autorecon/default-plugins/dns.py @@ -0,0 +1,57 @@ +from autorecon.plugins import ServiceScan + +class NmapDNS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap DNS' + self.tags = ['default', 'safe', 'dns'] + + def configure(self): + self.match_service_name('^domain') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}') + +class DNSZoneTransfer(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'DNS Zone Transfer' + self.tags = ['default', 'safe', 'dns'] + + def configure(self): + self.match_service_name('^domain') + + async def run(self, service): + if self.get_global('domain'): + await service.execute('dig AXFR -p {port} @{address} ' + self.get_global('domain'), outfile='{protocol}_{port}_dns_zone-transfer-domain.txt') + if service.target.type == 'hostname': + await service.execute('dig AXFR -p {port} @{address} {address}', outfile='{protocol}_{port}_dns_zone-transfer-hostname.txt') + await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') + +class DNSReverseLookup(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'DNS Reverse Lookup' + self.tags = ['default', 'safe', 'dns'] + + def configure(self): + self.match_service_name('^domain') + + async def run(self, service): + await service.execute('dig -p {port} -x {address} @{address}', outfile='{protocol}_{port}_dns_reverse-lookup.txt') + +class NmapMulticastDNS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Multicast DNS' + self.tags = ['default', 'safe', 'dns'] + + def configure(self): + self.match_service_name(['^mdns', '^zeroconf']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_multicastdns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_multicastdns_nmap.xml" {address}') diff --git a/autorecon/default-plugins/ftp.py b/autorecon/default-plugins/ftp.py new file mode 100644 index 0000000..edd470b --- /dev/null +++ b/autorecon/default-plugins/ftp.py @@ -0,0 +1,30 @@ +from autorecon.plugins import ServiceScan + +class NmapFTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap FTP' + self.tags = ['default', 'safe', 'ftp'] + + def configure(self): + self.match_service_name(['^ftp', '^ftp\-data']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}') + +class BruteforceFTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce FTP" + self.tags = ['default', 'ftp'] + + def configure(self): + self.match_service_name(['^ftp', '^ftp\-data']) + + def manual(self, service, plugin_was_run): + service.add_manual_commands('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{addressv6}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {addressv6}' + ]) diff --git a/autorecon/default-plugins/guess-port-scan.py b/autorecon/default-plugins/guess-port-scan.py new file mode 100644 index 0000000..6ae5fa9 --- /dev/null +++ b/autorecon/default-plugins/guess-port-scan.py @@ -0,0 +1,48 @@ +from autorecon.plugins import PortScan +from autorecon.targets import Service +import re + +class GuesPortScan(PortScan): + + def __init__(self): + super().__init__() + self.name = 'Guess TCP Ports' + self.type = 'tcp' + self.description = 'Performs an Nmap scan of the all TCP ports but guesses services based off the port found. Can be quicker. Proper service matching is performed at the end of the scan.' + self.tags = ['guess-port-scan', 'long'] + self.priority = 0 + + async def run(self, target): + if target.ports: + if target.ports['tcp']: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) + else: + return [] + else: + process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) + + insecure_ports = { + '20':'ftp', '21':'ftp', '22':'ssh', '23':'telnet', '25':'smtp', '53':'domain', '69':'tftp', '79':'finger', '80':'http', '88':'kerberos', '109':'pop3', '110':'pop3', '111':'rpcbind', '119':'nntp', '135':'msrpc', '139':'netbios-ssn', '143':'imap', '161':'snmp', '220':'imap', '389':'ldap', '433':'nntp', '445':'smb', '587':'smtp', '631':'ipp', '873':'rsync', '1098':'java-rmi', '1099':'java-rmi', '1433':'mssql', '1521':'oracle', '2049':'nfs', '2483':'oracle', '3020':'smb', '3306':'mysql', '3389':'rdp', '3632':'distccd', '5060':'asterisk', '5500':'vnc', '5900':'vnc', '5985':'wsman', '6379':'redis', '8080':'http-proxy', '27017':'mongod', '27018':'mongod', '27019':'mongod' + } + secure_ports = { + '443':'https', '465':'smtp', '563':'nntp', '585':'imaps', '593':'msrpc', '636':'ldap', '989':'ftp', '990':'ftp', '992':'telnet', '993':'imaps', '995':'pop3s', '2484':'oracle', '5061':'asterisk', '5986':'wsman' + } + + services = [] + while True: + line = await stdout.readline() + if line is not None: + match = re.match('^Discovered open port ([0-9]+)/tcp', line) + if match: + if match.group(1) in insecure_ports.keys(): + await target.add_service(Service('tcp', match.group(1), insecure_ports[match.group(1)])) + elif match.group(1) in secure_ports.keys(): + await target.add_service(Service('tcp', match.group(1), secure_ports[match.group(1)], True)) + service = target.extract_service(line) + if service is not None: + services.append(service) + else: + break + + await process.wait() + return services diff --git a/autorecon/default-plugins/http_server.py b/autorecon/default-plugins/http_server.py new file mode 100644 index 0000000..97bcc1f --- /dev/null +++ b/autorecon/default-plugins/http_server.py @@ -0,0 +1,214 @@ +from autorecon.plugins import ServiceScan +from autorecon.io import error, info, fformat +from shutil import which +import os + +class NmapHTTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap HTTP" + self.tags = ['default', 'safe', 'http'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + self.add_pattern('Server: ([^\n]+)', description='Identified HTTP Server: {match}') + self.add_pattern('WebDAV is ENABLED', description='WebDAV is enabled') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(http* or ssl*) and not (brute or broadcast or dos or external or http-slowloris* or fuzzer)" -oN "{scandir}/{protocol}_{port}_{http_scheme}_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_{http_scheme}_nmap.xml" {address}') + +class BruteforceHTTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce HTTP" + self.tags = ['default', 'http'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + def manual(self, service, plugin_was_run): + service.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{addressv6}/path/to/auth/area', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {addressv6} -m DIR:/path/to/auth/area', + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{addressv6}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {addressv6} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' + ]) + +class Curl(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Curl" + self.tags = ['default', 'safe', 'http'] + + def configure(self): + self.add_option("path", default="/", help="The path on the web server to curl. Default: %(default)s") + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + self.add_pattern('(?i)powered[ -]by[^\n]+') + + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('curl -sSik {http_scheme}://{addressv6}:{port}' + self.get_option('path'), outfile='{protocol}_{port}_{http_scheme}_curl.html') + +class CurlRobots(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Curl Robots" + self.tags = ['default', 'safe', 'http'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + async def run(self, service): + if service.protocol == 'tcp': + _, stdout, _ = await service.execute('curl -sSikf {http_scheme}://{addressv6}:{port}/robots.txt', future_outfile='{protocol}_{port}_{http_scheme}_curl-robots.txt') + lines = await stdout.readlines() + + if lines: + filename = fformat('{scandir}/{protocol}_{port}_{http_scheme}_curl-robots.txt') + with open(filename, mode='wt', encoding='utf8') as robots: + robots.write('\n'.join(lines)) + else: + info('{bblue}[' + fformat('{tag}') + ']{rst} There did not appear to be a robots.txt file in the webroot (/).') + +class DirBuster(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Directory Buster" + self.slug = 'dirbuster' + self.priority = 0 + self.tags = ['default', 'safe', 'long', 'http'] + + def configure(self): + self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') + self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web-Content/big.txt', '/usr/share/seclists/Discovery/Web-Content/raft-large-words.txt'], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') + self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') + self.add_option('ext', default='txt,html,php,asp,aspx,jsp', help='The extensions you wish to fuzz (no dot, comma separated). Default: %(default)s') + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + def check(self): + tool = self.get_option('tool') + if tool == 'feroxbuster': + if which('feroxbuster') is None: + error('The feroxbuster program could not be found. Make sure it is installed. (On Kali, run: sudo apt install feroxbuster)') + elif tool == 'gobuster': + if which('gobuster') is None: + error('The gobuster program could not be found. Make sure it is installed. (On Kali, run: sudo apt install gobuster)') + elif tool == 'dirsearch': + if which('dirsearch') is None: + error('The dirsearch program could not be found. Make sure it is installed. (On Kali, run: sudo apt install dirsearch)') + + async def run(self, service): + dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) + for wordlist in self.get_option('wordlist'): + name = os.path.splitext(os.path.basename(wordlist))[0] + if self.get_option('tool') == 'feroxbuster': + await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') + elif self.get_option('tool') == 'gobuster': + await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') + elif self.get_option('tool') == 'dirsearch': + if service.target.ipversion == 'IPv6': + error('dirsearch does not support IPv6.') + else: + await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -e "' + self.get_option('ext') + '" -f -q -w ' + wordlist + ' --format=plain -o "{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') + elif self.get_option('tool') == 'ffuf': + await service.execute('ffuf -u {http_scheme}://{addressv6}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') + elif self.get_option('tool') == 'dirb': + await service.execute('dirb {http_scheme}://{addressv6}:{port}/ ' + wordlist + ' -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') + + def manual(self, service, plugin_was_run): + dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) + if self.get_option('tool') == 'feroxbuster': + service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'feroxbuster -u {http_scheme}://{addressv6}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "' + self.get_option('ext') + '" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' + ]) + elif self.get_option('tool') == 'gobuster': + service.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ + 'gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' + ]) + elif self.get_option('tool') == 'dirsearch': + if service.target.ipversion == 'IPv4': + service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e "' + self.get_option('ext') + '" -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' + ]) + elif self.get_option('tool') == 'ffuf': + service.add_manual_command('(ffuf) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ + 'ffuf -u {http_scheme}://{addressv6}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_dirbuster.txt' + ]) + elif self.get_option('tool') == 'dirb': + service.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ + 'dirb {http_scheme}://{addressv6}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' + ]) + +class Nikto(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'nikto' + self.tags = ['default', 'safe', 'long', 'http'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + def manual(self, service, plugin_was_run): + if service.target.ipversion == 'IPv4': + service.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') + +class WhatWeb(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "whatweb" + self.tags = ['default', 'safe', 'http'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + async def run(self, service): + if service.protocol == 'tcp' and service.target.ipversion == 'IPv4': + await service.execute('whatweb --color=never --no-errors -a 3 -v {http_scheme}://{address}:{port} 2>&1', outfile='{protocol}_{port}_{http_scheme}_whatweb.txt') + +class WkHTMLToImage(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "wkhtmltoimage" + self.tags = ['default', 'safe', 'http'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + def check(self): + if which('wkhtmltoimage') is None: + error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') + + async def run(self, service): + if which('wkhtmltoimage') is not None: + if service.protocol == 'tcp': + await service.execute('wkhtmltoimage --format png {http_scheme}://{addressv6}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') + +class WPScan(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'WPScan' + self.tags = ['default', 'safe', 'http'] + + def configure(self): + self.match_service_name('^http') + self.match_service_name('^nacn_http$', negative_match=True) + + def manual(self, service, plugin_was_run): + service.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{addressv6}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') diff --git a/autorecon/default-plugins/kerberos.py b/autorecon/default-plugins/kerberos.py new file mode 100644 index 0000000..0830ff5 --- /dev/null +++ b/autorecon/default-plugins/kerberos.py @@ -0,0 +1,17 @@ +from autorecon.plugins import ServiceScan + +class NmapKerberos(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Kerberos" + self.tags = ['default', 'safe', 'kerberos', 'active-directory'] + + def configure(self): + self.match_service_name(['^kerberos', '^kpasswd']) + + async def run(self, service): + if self.get_global('domain') and self.get_global('username-wordlist'): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" --script-args krb5-enum-users.realm="' + self.get_global('domain') + '",userdb="' + self.get_global('username-wordlist') + '" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') + else: + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') diff --git a/autorecon/default-plugins/ldap.py b/autorecon/default-plugins/ldap.py new file mode 100644 index 0000000..8f01f4f --- /dev/null +++ b/autorecon/default-plugins/ldap.py @@ -0,0 +1,29 @@ +from autorecon.plugins import ServiceScan + +class NmapLDAP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap LDAP" + self.tags = ['default', 'safe', 'ldap', 'active-directory'] + + def configure(self): + self.match_service_name('^ldap') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ldap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ldap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ldap_nmap.xml" {address}') + +class LDAPSearch(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'LDAP Search' + self.tags = ['default', 'safe', 'ldap', 'active-directory'] + + def configure(self): + self.match_service_name('^ldap') + + def manual(self, service, plugin_was_run): + service.add_manual_command('ldapsearch command (modify before running):', [ + 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' + ]) diff --git a/autorecon/default-plugins/misc.py b/autorecon/default-plugins/misc.py new file mode 100644 index 0000000..4775909 --- /dev/null +++ b/autorecon/default-plugins/misc.py @@ -0,0 +1,188 @@ +from autorecon.plugins import ServiceScan +from autorecon.io import fformat + +class NmapCassandra(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Cassandra" + self.tags = ['default', 'safe', 'cassandra'] + + def configure(self): + self.match_service_name('^apani1') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cassandra* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cassandra_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cassandra_nmap.xml" {address}') + +class NmapCUPS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap CUPS" + self.tags = ['default', 'safe', 'cups'] + + def configure(self): + self.match_service_name('^ipp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cups* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cups_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cups_nmap.xml" {address}') + +class NmapDistccd(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap distccd" + self.tags = ['default', 'safe', 'distccd'] + + def configure(self): + self.match_service_name('^distccd') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,distcc-cve2004-2687" --script-args="distcc-cve2004-2687.cmd=id" -oN "{scandir}/{protocol}_{port}_distcc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_distcc_nmap.xml" {address}') + +class NmapFinger(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap finger" + self.tags = ['default', 'safe', 'finger'] + + def configure(self): + self.match_service_name('^finger') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,finger" -oN "{scandir}/{protocol}_{port}_finger_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_finger_nmap.xml" {address}') + +class NmapIMAP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap IMAP" + self.tags = ['default', 'safe', 'imap', 'email'] + + def configure(self): + self.match_service_name('^imap') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}') + +class NmapIrc(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap IRC' + self.tags = ['default', 'safe', 'irc'] + + def configure(self): + self.match_service_name('^irc') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV --script irc-botnet-channels,irc-info,irc-unrealircd-backdoor -oN "{scandir}/{protocol}_{port}_irc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_irc_nmap.xml" -p {port} {address}') + +class NmapNNTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap NNTP" + self.tags = ['default', 'safe', 'nntp'] + + def configure(self): + self.match_service_name('^nntp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nntp-ntlm-info" -oN "{scandir}/{protocol}_{port}_nntp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nntp_nmap.xml" {address}') + +class NmapPOP3(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap POP3" + self.tags = ['default', 'safe', 'pop3', 'email'] + + def configure(self): + self.match_service_name('^pop3') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(pop3* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_pop3_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_pop3_nmap.xml" {address}') + +class NmapRMI(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap RMI" + self.tags = ['default', 'safe', 'rmi'] + + def configure(self): + self.match_service_name(['^java\-rmi', '^rmiregistry']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}') + +class NmapTelnet(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Telnet' + self.tags = ['default', 'safe', 'telnet'] + + def configure(self): + self.match_service_name('^telnet') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,telnet-encryption,telnet-ntlm-info" -oN "{scandir}/{protocol}_{port}_telnet-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_telnet_nmap.xml" {address}') + +class NmapTFTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap TFTP' + self.tags = ['default', 'safe', 'tftp'] + + def configure(self): + self.match_service_name('^tftp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,tftp-enum" -oN "{scandir}/{protocol}_{port}_tftp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_tftp_nmap.xml" {address}') + +class NmapVNC(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap VNC' + self.tags = ['default', 'safe', 'vnc'] + + def configure(self): + self.match_service_name('^vnc') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}') + +class WinRMDetection(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'WinRM Detection' + self.tags = ['default', 'safe', 'winrm'] + + def configure(self): + self.match_service_name('^wsman') + self.match_service('tcp', [5985, 5986], '^http') + + async def run(self, service): + filename = fformat('{scandir}/{protocol}_{port}_winrm-detection.txt') + with open(filename, mode='wt', encoding='utf8') as winrm: + winrm.write('WinRM was possibly detected running on ' + service.protocol + ' port ' + str(service.port) + '.\nCheck _manual_commands.txt for manual commands you can run against this service.') + + def manual(self, service, plugin_was_run): + service.add_manual_commands('Bruteforce logins:', [ + 'crackmapexec winrm {address} -d ' + self.get_global('domain', default='') + ' -u ' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + ' -p ' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + ]) + + service.add_manual_commands('Check login (requires credentials):', [ + 'crackmapexec winrm {address} -d ' + self.get_global('domain', default='') + ' -u -p -x "whoami"' + ]) + + service.add_manual_commands('Evil WinRM (gem install evil-winrm):', [ + 'evil-winrm -u -p -i {address}', + 'evil-winrm -u -H -i {address}' + ]) diff --git a/autorecon/default-plugins/nfs.py b/autorecon/default-plugins/nfs.py new file mode 100644 index 0000000..515a2ec --- /dev/null +++ b/autorecon/default-plugins/nfs.py @@ -0,0 +1,40 @@ +from autorecon.plugins import ServiceScan + +class NmapNFS(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap NFS" + self.tags = ['default', 'safe', 'nfs'] + + def configure(self): + self.match_service_name(['^nfs', '^rpcbind']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rpcinfo or nfs*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_nfs_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nfs_nmap.xml" {address}') + +class Showmount(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "showmount" + self.tags = ['default', 'safe', 'nfs'] + + def configure(self): + self.match_service_name(['^nfs', '^rpcbind']) + + async def run(self, service): + await service.execute('showmount -e {address} 2>&1', outfile='{protocol}_{port}_showmount.txt') + +class NmapMountd(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap Mountd" + self.tags = ['default', 'safe', 'nfs'] + + def configure(self): + self.match_service_name('^mountd') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nfs* and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mountd_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mountd_nmap.xml" {address}') diff --git a/autorecon/default-plugins/rdp.py b/autorecon/default-plugins/rdp.py new file mode 100644 index 0000000..e99334f --- /dev/null +++ b/autorecon/default-plugins/rdp.py @@ -0,0 +1,30 @@ +from autorecon.plugins import ServiceScan + +class NmapRDP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap RDP" + self.tags = ['default', 'safe', 'rdp'] + + def configure(self): + self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rdp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rdp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rdp_nmap.xml" {address}') + +class BruteforceRDP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce RDP" + self.tags = ['default', 'rdp'] + + def configure(self): + self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) + + def manual(self, service, plugin_was_run): + service.add_manual_commands('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{addressv6}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {addressv6}' + ]) diff --git a/autorecon/default-plugins/redis.py b/autorecon/default-plugins/redis.py new file mode 100644 index 0000000..4603904 --- /dev/null +++ b/autorecon/default-plugins/redis.py @@ -0,0 +1,37 @@ +from autorecon.plugins import ServiceScan +from autorecon.io import error +from shutil import which + +class NmapRedis(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Redis' + self.tags = ['default', 'safe', 'redis'] + + def configure(self): + self.match_service_name('^redis$') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,redis-info" -oN "{scandir}/{protocol}_{port}_redis_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_redis_nmap.xml" {address}') + +class RedisCli(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Redis Cli' + self.tags = ['default', 'safe', 'redis'] + + def configure(self): + self.match_service_name('^redis$') + + def check(self): + if which('redis-cli') is None: + error('The redis-cli program could not be found. Make sure it is installed. (On Kali, run: sudo apt install redis-tools)') + + async def run(self, service): + if which('redis-cli') is not None: + _, stdout, _ = await service.execute('redis-cli -p {port} -h {address} INFO', outfile='{protocol}_{port}_redis_info.txt') + if not (await stdout.readline()).startswith('NOAUTH Authentication required'): + await service.execute('redis-cli -p {port} -h {address} CONFIG GET \'*\'', outfile='{protocol}_{port}_redis_config.txt') + await service.execute('redis-cli -p {port} -h {address} CLIENT LIST', outfile='{protocol}_{port}_redis_client-list.txt') diff --git a/autorecon/default-plugins/reporting.py b/autorecon/default-plugins/reporting.py new file mode 100644 index 0000000..e5b87be --- /dev/null +++ b/autorecon/default-plugins/reporting.py @@ -0,0 +1,163 @@ +from autorecon.plugins import Report +from autorecon.config import config +from xml.sax.saxutils import escape +import os, glob + +class CherryTree(Report): + + def __init__(self): + super().__init__() + self.name = 'CherryTree' + self.tags = [] + + async def run(self, targets): + if len(targets) > 1: + report = os.path.join(config['outdir'], 'report.xml.ctd') + elif len(targets) == 1: + report = os.path.join(targets[0].reportdir, 'report.xml.ctd') + else: + return + + with open(report, 'w') as output: + output.writelines('\n\n') + for target in targets: + output.writelines('\n') + + files = [os.path.abspath(filename) for filename in glob.iglob(os.path.join(target.scandir, '**/*'), recursive=True) if os.path.isfile(filename) and filename.endswith(('.txt', '.html'))] + + if target.scans['ports']: + output.writelines('\n') + for scan in target.scans['ports'].keys(): + if len(target.scans['ports'][scan]['commands']) > 0: + output.writelines('\n') + for command in target.scans['ports'][scan]['commands']: + output.writelines('' + escape(command[0])) + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n' + escape(filename) + ':\n\n') + with open(filename, 'r') as file: + output.writelines(escape(file.read()) + '\n') + output.writelines('\n') + output.writelines('\n') + output.writelines('\n') + if target.scans['services']: + output.writelines('\n') + for service in target.scans['services'].keys(): + output.writelines('\n') + for plugin in target.scans['services'][service].keys(): + if len(target.scans['services'][service][plugin]['commands']) > 0: + output.writelines('\n') + for command in target.scans['services'][service][plugin]['commands']: + output.writelines('' + escape(command[0])) + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n' + escape(filename) + ':\n\n') + with open(filename, 'r') as file: + output.writelines(escape(file.read()) + '\n') + output.writelines('\n') + output.writelines('\n') + output.writelines('\n') + output.writelines('\n') + + manual_commands = os.path.join(target.scandir, '_manual_commands.txt') + if os.path.isfile(manual_commands): + output.writelines('\n') + with open(manual_commands, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + + patterns = os.path.join(target.scandir, '_patterns.log') + if os.path.isfile(patterns): + output.writelines('\n') + with open(patterns, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + + commands = os.path.join(target.scandir, '_commands.log') + if os.path.isfile(commands): + output.writelines('\n') + with open(commands, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + + errors = os.path.join(target.scandir, '_errors.log') + if os.path.isfile(errors): + output.writelines('\n') + with open(errors, 'r') as file: + output.writelines('' + escape(file.read()) + '\n') + output.writelines('\n') + output.writelines('\n') + + output.writelines('') + +class Markdown(Report): + + def __init__(self): + super().__init__() + self.name = 'Markdown' + + async def run(self, targets): + if len(targets) > 1: + report = os.path.join(config['outdir'], 'report.md') + elif len(targets) == 1: + report = os.path.join(targets[0].reportdir, 'report.md') + else: + return + + os.makedirs(report, exist_ok=True) + + for target in targets: + os.makedirs(os.path.join(report, target.address), exist_ok=True) + + files = [os.path.abspath(filename) for filename in glob.iglob(os.path.join(target.scandir, '**/*'), recursive=True) if os.path.isfile(filename) and filename.endswith(('.txt', '.html'))] + + if target.scans['ports']: + os.makedirs(os.path.join(report, target.address, 'Port Scans'), exist_ok=True) + for scan in target.scans['ports'].keys(): + if len(target.scans['ports'][scan]['commands']) > 0: + with open(os.path.join(report, target.address, 'Port Scans', 'PortScan - ' + target.scans['ports'][scan]['plugin'].name + '.md'), 'w') as output: + for command in target.scans['ports'][scan]['commands']: + output.writelines('```bash\n' + command[0] + '\n```') + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n[' + filename + '](file://' + filename + '):\n\n') + with open(filename, 'r') as file: + output.writelines('```\n' + file.read() + '\n```\n') + if target.scans['services']: + os.makedirs(os.path.join(report, target.address, 'Services'), exist_ok=True) + for service in target.scans['services'].keys(): + os.makedirs(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-')), exist_ok=True) + for plugin in target.scans['services'][service].keys(): + if len(target.scans['services'][service][plugin]['commands']) > 0: + with open(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-'), target.scans['services'][service][plugin]['plugin'].name + '.md'), 'w') as output: + for command in target.scans['services'][service][plugin]['commands']: + output.writelines('```bash\n' + command[0] + '\n```') + for filename in files: + if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): + output.writelines('\n\n[' + filename + '](file://' + filename + '):\n\n') + with open(filename, 'r') as file: + output.writelines('```\n' + file.read() + '\n```\n') + + manual_commands = os.path.join(target.scandir, '_manual_commands.txt') + if os.path.isfile(manual_commands): + with open(os.path.join(report, target.address, 'Manual Commands' + '.md'), 'w') as output: + with open(manual_commands, 'r') as file: + output.writelines('```bash\n' + file.read() + '\n```') + + patterns = os.path.join(target.scandir, '_patterns.log') + if os.path.isfile(patterns): + with open(os.path.join(report, target.address, 'Patterns' + '.md'), 'w') as output: + with open(patterns, 'r') as file: + output.writelines(file.read()) + + commands = os.path.join(target.scandir, '_commands.log') + if os.path.isfile(commands): + with open(os.path.join(report, target.address, 'Commands' + '.md'), 'w') as output: + with open(commands, 'r') as file: + output.writelines('```bash\n' + file.read() + '\n```') + + errors = os.path.join(target.scandir, '_errors.log') + if os.path.isfile(errors): + with open(os.path.join(report, target.address, 'Errors' + '.md'), 'w') as output: + with open(errors, 'r') as file: + output.writelines('```\n' + file.read() + '\n```') diff --git a/autorecon/default-plugins/rpc.py b/autorecon/default-plugins/rpc.py new file mode 100644 index 0000000..429c499 --- /dev/null +++ b/autorecon/default-plugins/rpc.py @@ -0,0 +1,42 @@ +from autorecon.plugins import ServiceScan +from autorecon.io import error, warn + +class NmapRPC(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap MSRPC" + self.tags = ['default', 'rpc'] + + def configure(self): + self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,msrpc-enum,rpc-grind,rpcinfo" -oN "{scandir}/{protocol}_{port}_rpc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rpc_nmap.xml" {address}') + +class RPCClient(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "rpcclient" + self.tags = ['default', 'safe', 'rpc'] + + def configure(self): + self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) + + def manual(self, service, plugin_was_run): + service.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') + +class RPCDump(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'rpcdump' + self.tags = ['default', 'safe', 'rpc'] + + def configure(self): + self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) + + async def run(self, service): + if service.protocol == 'tcp': + await service.execute('impacket-rpcdump -port {port} {address}', outfile='{protocol}_{port}_rpc_rpcdump.txt') diff --git a/autorecon/default-plugins/rsync.py b/autorecon/default-plugins/rsync.py new file mode 100644 index 0000000..da9a4be --- /dev/null +++ b/autorecon/default-plugins/rsync.py @@ -0,0 +1,27 @@ +from autorecon.plugins import ServiceScan + +class NmapRsync(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Nmap Rsync' + self.tags = ['default', 'safe', 'rsync'] + + def configure(self): + self.match_service_name('^rsync') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rsync* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rsync_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rsync_nmap.xml" {address}') + +class RsyncList(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'Rsync List Files' + self.tags = ['default', 'safe', 'rsync'] + + def configure(self): + self.match_service_name('^rsync') + + async def run(self, service): + await service.execute('rsync -av --list-only rsync://{addressv6}:{port}', outfile='{protocol}_{port}_rsync_file_list.txt') diff --git a/autorecon/default-plugins/sip.py b/autorecon/default-plugins/sip.py new file mode 100644 index 0000000..8b60f7e --- /dev/null +++ b/autorecon/default-plugins/sip.py @@ -0,0 +1,28 @@ +from autorecon.plugins import ServiceScan + +class NmapSIP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SIP" + self.tags = ['default', 'safe', 'sip'] + + def configure(self): + self.match_service_name('^asterisk') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,sip-enum-users,sip-methods" -oN "{scandir}/{protocol}_{port}_sip_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_sip_nmap.xml" {address}') + +class SIPVicious(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SIPVicious" + self.tags = ['default', 'safe', 'sip'] + + def configure(self): + self.match_service_name('^asterisk') + + def manual(self, service, plugin_was_run): + if service.target.ipversion == 'IPv4': + service.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') diff --git a/autorecon/default-plugins/smb.py b/autorecon/default-plugins/smb.py new file mode 100644 index 0000000..e5319ab --- /dev/null +++ b/autorecon/default-plugins/smb.py @@ -0,0 +1,104 @@ +from autorecon.plugins import ServiceScan + +class NmapSMB(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SMB" + self.tags = ['default', 'safe', 'smb', 'active-directory'] + + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}') + +class SMBVuln(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SMB Vulnerabilities" + self.tags = ['unsafe', 'smb', 'active-directory'] + + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}') + await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}') + await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}') + + def manual(self, service, plugin_was_run): + if not plugin_was_run: # Only suggest these if they weren't run. + service.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', + 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' + ]) + +class Enum4Linux(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Enum4Linux" + self.tags = ['default', 'safe', 'active-directory'] + + def configure(self): + self.match_service_name(['^ldap', '^smb', '^microsoft\-ds', '^netbios']) + self.match_port('tcp', [139, 389, 445]) + self.match_port('udp', 137) + self.run_once(True) + + async def run(self, service): + if service.target.ipversion == 'IPv4': + await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') + +class NBTScan(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "nbtscan" + self.tags = ['default', 'safe', 'netbios', 'active-directory'] + + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + self.match_port('udp', 137) + self.run_once(True) + + async def run(self, service): + if service.target.ipversion == 'IPv4': + await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') + +class SMBClient(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SMBClient" + self.tags = ['default', 'safe', 'smb', 'active-directory'] + + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + self.match_port('tcp', [139, 445]) + self.run_once(True) + + async def run(self, service): + await service.execute('smbclient -L\\\\ -N -I {address} 2>&1', outfile='smbclient.txt') + +class SMBMap(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SMBMap" + self.tags = ['default', 'safe', 'smb', 'active-directory'] + + def configure(self): + self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) + + async def run(self, service): + if service.target.ipversion == 'IPv4': + await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') + await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') + await service.execute('smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') + await service.execute('smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') diff --git a/autorecon/default-plugins/smtp.py b/autorecon/default-plugins/smtp.py new file mode 100644 index 0000000..705c154 --- /dev/null +++ b/autorecon/default-plugins/smtp.py @@ -0,0 +1,33 @@ +from autorecon.plugins import ServiceScan + +class NmapSMTP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SMTP" + self.tags = ['default', 'safe', 'smtp', 'email'] + + def configure(self): + self.match_service_name('^smtp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}') + +class SMTPUserEnum(ServiceScan): + + def __init__(self): + super().__init__() + self.name = 'SMTP-User-Enum' + self.tags = ['default', 'safe', 'smtp', 'email'] + + def configure(self): + self.match_service_name('^smtp') + + async def run(self, service): + await service.execute('hydra smtp-enum://{addressv6}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') + await service.execute('hydra smtp-enum://{addressv6}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') + + def manual(self, service, plugin_was_run): + service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ + 'hydra smtp-enum://{addressv6}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' + ]) diff --git a/autorecon/default-plugins/snmp.py b/autorecon/default-plugins/snmp.py new file mode 100644 index 0000000..7b104a0 --- /dev/null +++ b/autorecon/default-plugins/snmp.py @@ -0,0 +1,53 @@ +from autorecon.plugins import ServiceScan + +class NmapSNMP(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SNMP" + self.tags = ['default', 'safe', 'snmp'] + + def configure(self): + self.match_service_name('^snmp') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(snmp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_snmp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_snmp_nmap.xml" {address}') + +class OneSixtyOne(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "OneSixtyOne" + self.tags = ['default', 'safe', 'snmp'] + + def configure(self): + self.match_service_name('^snmp') + self.match_port('udp', 161) + self.run_once(True) + self.add_option('community-strings', default='/usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt', help='The file containing a list of community strings to try. Default: %(default)s') + + async def run(self, service): + if service.target.ipversion == 'IPv4': + await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') + +class SNMPWalk(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SNMPWalk" + self.tags = ['default', 'safe', 'snmp'] + + def configure(self): + self.match_service_name('^snmp') + self.match_port('udp', 161) + self.run_once(True) + + async def run(self, service): + await service.execute('snmpwalk -c public -v 1 {address} 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.1.6.0 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_system_processes.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.2 2>&1', outfile='{scandir}/{protocol}_{port}_snmp_snmpwalk_running_processes.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_process_paths.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_storage_units.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_software_names.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.4.1.77.1.2.25 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_user_accounts.txt') + await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.6.13.1.3 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_tcp_ports.txt') diff --git a/autorecon/default-plugins/ssh.py b/autorecon/default-plugins/ssh.py new file mode 100644 index 0000000..2ecedab --- /dev/null +++ b/autorecon/default-plugins/ssh.py @@ -0,0 +1,30 @@ +from autorecon.plugins import ServiceScan + +class NmapSSH(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Nmap SSH" + self.tags = ['default', 'safe', 'ssh'] + + def configure(self): + self.match_service_name('^ssh') + + async def run(self, service): + await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,ssh2-enum-algos,ssh-hostkey,ssh-auth-methods" -oN "{scandir}/{protocol}_{port}_ssh_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ssh_nmap.xml" {address}') + +class BruteforceSSH(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "Bruteforce SSH" + self.tags = ['default', 'ssh'] + + def configure(self): + self.match_service_name('ssh') + + def manual(self, service, plugin_was_run): + service.add_manual_command('Bruteforce logins:', [ + 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{addressv6}', + 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {addressv6}' + ]) diff --git a/autorecon/default-plugins/sslscan.py b/autorecon/default-plugins/sslscan.py new file mode 100644 index 0000000..43071ac --- /dev/null +++ b/autorecon/default-plugins/sslscan.py @@ -0,0 +1,16 @@ +from autorecon.plugins import ServiceScan + +class SSLScan(ServiceScan): + + def __init__(self): + super().__init__() + self.name = "SSL Scan" + self.tags = ['default', 'safe', 'ssl', 'tls'] + + def configure(self): + self.match_all_service_names(True) + self.require_ssl(True) + + async def run(self, service): + if service.protocol == 'tcp' and service.secure: + await service.execute('sslscan --show-certificate --no-colour {addressv6}:{port} 2>&1', outfile='{protocol}_{port}_sslscan.html') diff --git a/autorecon/global.toml b/autorecon/global.toml new file mode 100644 index 0000000..5ef1da5 --- /dev/null +++ b/autorecon/global.toml @@ -0,0 +1,22 @@ +[global.username-wordlist] +default = '/usr/share/seclists/Usernames/top-usernames-shortlist.txt' +help = 'A wordlist of usernames, useful for bruteforcing. Default: %(default)s' + +[global.password-wordlist] +default = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' +help = 'A wordlist of passwords, useful for bruteforcing. Default: %(default)s' + +[global.domain] +help = 'The domain to use (if known). Used for DNS and/or Active Directory. Default: %(default)s' + +# Configure global pattern matching here. +[[pattern]] +description = 'Nmap script found a potential vulnerability. ({match})' +pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' + +[[pattern]] +pattern = '(?i)unauthorized' + +[[pattern]] +description = 'CVE Identified: {match}' +pattern = '(CVE-\d{4}-\d{4,7})' diff --git a/autorecon.py b/autorecon/main.py similarity index 98% rename from autorecon.py rename to autorecon/main.py index 950bb08..a191731 100644 --- a/autorecon.py +++ b/autorecon/main.py @@ -1,10 +1,10 @@ #!/usr/bin/python3 -import argparse, asyncio, importlib, inspect, ipaddress, math, os, re, sys, signal, select, socket, termios, time, traceback, tty +import argparse, asyncio, importlib, inspect, ipaddress, math, os, re, select, shutil, signal, socket, sys, termios, time, traceback, tty from datetime import datetime try: - import colorama, toml, unidecode + import appdirs, colorama, toml, unidecode from colorama import Fore, Style except ModuleNotFoundError: print('One or more required modules was not installed. Please run or re-run: ' + ('sudo ' if os.getuid() == 0 else '') + 'python3 -m pip install -r requirements.txt') @@ -17,6 +17,16 @@ from autorecon.io import slugify, e, fformat, cprint, debug, info, warn, error, from autorecon.plugins import Pattern, PortScan, ServiceScan, Report, AutoRecon from autorecon.targets import Target, Service +def install(): + shutil.rmtree(config['config_dir'], ignore_errors=True) + os.makedirs(config['config_dir'], exist_ok=True) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) + shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) + shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default-plugins'), os.path.join(config['config_dir'], 'plugins')) + +if not os.path.exists(config['config_dir']): + install() + # Save current terminal settings so we can restore them. terminal_settings = termios.tcgetattr(sys.stdin.fileno()) @@ -711,15 +721,15 @@ async def scan_target(target): autorecon.completed_targets.append(target) autorecon.scanning_targets.remove(target) -async def main(): +async def run(): parser = argparse.ArgumentParser(add_help=False, description='Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets.') parser.add_argument('targets', action='store', help='IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan.', nargs='*') parser.add_argument('-t', '--targets', action='store', type=str, default='', dest='target_file', help='Read targets from file.') parser.add_argument('-p', '--ports', action='store', type=str, help='Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both TCP/UDP, put port(s) at start or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: %(default)s') parser.add_argument('-m', '--max-scans', action='store', type=int, help='The maximum number of concurrent scans to run. Default: %(default)s') parser.add_argument('-mp', '--max-port-scans', action='store', type=int, help='The maximum number of concurrent port scans to run. Default: 10 (approx 20%% of max-scans unless specified)') - parser.add_argument('-c', '--config', action='store', type=str, default=os.path.dirname(os.path.realpath(__file__)) + '/config.toml', dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') - parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.dirname(os.path.realpath(__file__)) + '/global.toml') + parser.add_argument('-c', '--config', action='store', type=str, default=os.path.join(config['config_dir'], 'config.toml'), dest='config_file', help='Location of AutoRecon\'s config file. Default: %(default)s') + parser.add_argument('-g', '--global-file', action='store', type=str, dest='global_file', help='Location of AutoRecon\'s global file. Default: ' + os.path.join(config['config_dir'], 'global.toml')) parser.add_argument('--tags', action='store', type=str, default='default', help='Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--exclude-tags', action='store', type=str, default='', metavar='TAGS', help='Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in at least one group. Default: %(default)s') parser.add_argument('--port-scans', action='store', type=str, metavar='PLUGINS', help='Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: %(default)s') @@ -753,7 +763,7 @@ async def main(): autorecon.argparse = parser if args.version: - print('AutoRecon v2.0-beta3') + print('AutoRecon v2.0') sys.exit(0) # Parse config file and args for global.toml first. @@ -1368,12 +1378,15 @@ async def main(): # Restore original terminal settings. termios.tcsetattr(sys.stdin, termios.TCSADRAIN, terminal_settings) -if __name__ == '__main__': +def main(): # Capture Ctrl+C and cancel everything. signal.signal(signal.SIGINT, cancel_all_tasks) try: - asyncio.run(main()) + asyncio.run(run()) except asyncio.exceptions.CancelledError: pass except RuntimeError: pass + +if __name__ == '__main__': + main() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..eeaf603 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,54 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "unidecode" +version = "1.3.1" +description = "ASCII transliterations of Unicode text" +category = "main" +optional = false +python-versions = ">=3.5" + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "681db41aa556d6d3f79e1e8ee0107bccd078e39c8db7e6e0159860c96ea93c5b" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +unidecode = [ + {file = "Unidecode-1.3.1-py3-none-any.whl", hash = "sha256:5f58926b9125b499f8ab6816828e737578fa3e31fa24d351a3ab7f4b7c064ab0"}, + {file = "Unidecode-1.3.1.tar.gz", hash = "sha256:6efac090bf8f29970afc90caf4daae87b172709b786cb1b4da2d0c0624431ecc"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3b4fc3f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.poetry] +name = "autorecon" +version = "2.0.0" +description = "A multi-threaded network reconnaissance tool which performs automated enumeration of services." +authors = ["Tib3rius"] +license = "GNU GPL v3" + +[tool.poetry.dependencies] +python = "^3.7" +appdirs = "^1.4.4" +colorama = "^0.4.4" +toml = "^0.10.2" +Unidecode = "^1.3.1" + +[tool.poetry.dev-dependencies] + +[tool.poetry.scripts] +autorecon = "autorecon.main:main" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt index d363a63..6a3fb1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -unidecode -toml +appdirs colorama +toml +unidecode From ba5b684c9fe7eb3cbd7a80eba6e52f833551d999 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 14 Sep 2021 15:52:04 -0400 Subject: [PATCH 92/95] Removed old config and global toml files. --- config.toml | 19 ------------------- global.toml | 22 ---------------------- 2 files changed, 41 deletions(-) delete mode 100644 config.toml delete mode 100644 global.toml diff --git a/config.toml b/config.toml deleted file mode 100644 index a9df31d..0000000 --- a/config.toml +++ /dev/null @@ -1,19 +0,0 @@ -# Configure regular AutoRecon options at the top of this file. - -create-port-dirs = true -nmap-append = '-T4' -# verbose = 1 -# max-scans = 30 - -# Configure global options here. -# [global] -# username-wordlist = '/usr/share/seclists/Usernames/cirt-default-usernames.txt' - -# Configure plugin options here. -# [dirbuster] -# threads = 50 -# wordlist = [ -# '/usr/share/seclists/Discovery/Web-Content/common.txt', -# '/usr/share/seclists/Discovery/Web-Content/big.txt', -# '/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt' -# ] diff --git a/global.toml b/global.toml deleted file mode 100644 index 5ef1da5..0000000 --- a/global.toml +++ /dev/null @@ -1,22 +0,0 @@ -[global.username-wordlist] -default = '/usr/share/seclists/Usernames/top-usernames-shortlist.txt' -help = 'A wordlist of usernames, useful for bruteforcing. Default: %(default)s' - -[global.password-wordlist] -default = '/usr/share/seclists/Passwords/darkweb2017-top100.txt' -help = 'A wordlist of passwords, useful for bruteforcing. Default: %(default)s' - -[global.domain] -help = 'The domain to use (if known). Used for DNS and/or Active Directory. Default: %(default)s' - -# Configure global pattern matching here. -[[pattern]] -description = 'Nmap script found a potential vulnerability. ({match})' -pattern = 'State: (?:(?:LIKELY\_?)?VULNERABLE)' - -[[pattern]] -pattern = '(?i)unauthorized' - -[[pattern]] -description = 'CVE Identified: {match}' -pattern = '(CVE-\d{4}-\d{4,7})' From 2b0860c377218fbd5abd6a5789c2e89e81d8f946 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 14 Sep 2021 17:42:50 -0400 Subject: [PATCH 93/95] More poetry magic. --- autorecon.py | 4 ++++ autorecon/main.py | 10 ++++++++-- pyproject.toml | 4 ++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 autorecon.py diff --git a/autorecon.py b/autorecon.py new file mode 100644 index 0000000..cd7021d --- /dev/null +++ b/autorecon.py @@ -0,0 +1,4 @@ +from autorecon.main import main + +if __name__ == '__main__': + main() diff --git a/autorecon/main.py b/autorecon/main.py index a191731..48a5dbe 100644 --- a/autorecon/main.py +++ b/autorecon/main.py @@ -17,15 +17,21 @@ from autorecon.io import slugify, e, fformat, cprint, debug, info, warn, error, from autorecon.plugins import Pattern, PortScan, ServiceScan, Report, AutoRecon from autorecon.targets import Target, Service +VERSION = "2.0.0" + def install(): - shutil.rmtree(config['config_dir'], ignore_errors=True) + shutil.rmtree(config['config_dir'], ignore_errors=True, onerror=None) os.makedirs(config['config_dir'], exist_ok=True) + open(os.path.join(config['config_dir'], 'VERSION-' + VERSION), 'a').close() shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.toml'), os.path.join(config['config_dir'], 'config.toml')) shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'global.toml'), os.path.join(config['config_dir'], 'global.toml')) shutil.copytree(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default-plugins'), os.path.join(config['config_dir'], 'plugins')) if not os.path.exists(config['config_dir']): install() +else: + if not os.path.exists(os.path.join(config['config_dir'], 'VERSION-' + VERSION)): + pass # Save current terminal settings so we can restore them. terminal_settings = termios.tcgetattr(sys.stdin.fileno()) @@ -763,7 +769,7 @@ async def run(): autorecon.argparse = parser if args.version: - print('AutoRecon v2.0') + print('AutoRecon v' + VERSION) sys.exit(0) # Parse config file and args for global.toml first. diff --git a/pyproject.toml b/pyproject.toml index 3b4fc3f..1a12768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,10 @@ version = "2.0.0" description = "A multi-threaded network reconnaissance tool which performs automated enumeration of services." authors = ["Tib3rius"] license = "GNU GPL v3" +exclude = ["autorecon.py"] +packages = [ + { include = "autorecon" }, +] [tool.poetry.dependencies] python = "^3.7" From d1ec130609e24028e434acbe35c121c8179d958c Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 14 Sep 2021 17:58:40 -0400 Subject: [PATCH 94/95] Removed old plugins directory. --- .gitignore | 1 - plugins/__init__.py | 0 plugins/databases.py | 135 ---------------------- plugins/default-port-scan.py | 103 ----------------- plugins/dns.py | 57 ---------- plugins/ftp.py | 30 ----- plugins/guess-port-scan.py | 48 -------- plugins/http_server.py | 214 ----------------------------------- plugins/kerberos.py | 17 --- plugins/ldap.py | 29 ----- plugins/misc.py | 188 ------------------------------ plugins/nfs.py | 40 ------- plugins/rdp.py | 30 ----- plugins/redis.py | 37 ------ plugins/reporting.py | 163 -------------------------- plugins/rpc.py | 42 ------- plugins/rsync.py | 27 ----- plugins/sip.py | 28 ----- plugins/smb.py | 104 ----------------- plugins/smtp.py | 33 ------ plugins/snmp.py | 53 --------- plugins/ssh.py | 30 ----- plugins/sslscan.py | 16 --- 23 files changed, 1425 deletions(-) delete mode 100644 plugins/__init__.py delete mode 100644 plugins/databases.py delete mode 100644 plugins/default-port-scan.py delete mode 100644 plugins/dns.py delete mode 100644 plugins/ftp.py delete mode 100644 plugins/guess-port-scan.py delete mode 100644 plugins/http_server.py delete mode 100644 plugins/kerberos.py delete mode 100644 plugins/ldap.py delete mode 100644 plugins/misc.py delete mode 100644 plugins/nfs.py delete mode 100644 plugins/rdp.py delete mode 100644 plugins/redis.py delete mode 100644 plugins/reporting.py delete mode 100644 plugins/rpc.py delete mode 100644 plugins/rsync.py delete mode 100644 plugins/sip.py delete mode 100644 plugins/smb.py delete mode 100644 plugins/smtp.py delete mode 100644 plugins/snmp.py delete mode 100644 plugins/ssh.py delete mode 100644 plugins/sslscan.py diff --git a/.gitignore b/.gitignore index 463d071..a89c11f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ __pycache__ -plugins/__pycache__ *.pyc results/ diff --git a/plugins/__init__.py b/plugins/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/databases.py b/plugins/databases.py deleted file mode 100644 index 4e5286b..0000000 --- a/plugins/databases.py +++ /dev/null @@ -1,135 +0,0 @@ -from autorecon.plugins import ServiceScan -from autorecon.io import error -from shutil import which - -class NmapMongoDB(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap MongoDB" - self.tags = ['default', 'safe', 'databases'] - - def configure(self): - self.match_service_name('^mongod') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mongodb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mongodb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mongodb_nmap.xml" {address}') - -class NmapMSSQL(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap MSSQL" - self.tags = ['default', 'safe', 'databases'] - - def configure(self): - self.match_service_name(['^mssql', '^ms\-sql']) - - def manual(self, service, plugin_was_run): - if service.target.ipversion == 'IPv4': - service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ms-sql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="mssql.instance-port={port},mssql.username=sa,mssql.password=sa" -oN "{scandir}/{protocol}_{port}_mssql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mssql_nmap.xml" {address}') - -class NmapMYSQL(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap MYSQL" - self.tags = ['default', 'safe', 'databases'] - - def configure(self): - self.match_service_name('^mysql') - - def manual(self, service, plugin_was_run): - if service.target.ipversion == 'IPv4': - service.add_manual_command('(sqsh) interactive database shell:', 'sqsh -U -P -S {address}:{port}') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(mysql* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mysql_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mysql_nmap.xml" {address}') - -class NmapOracle(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap Oracle" - self.tags = ['default', 'safe', 'databases'] - - def configure(self): - self.match_service_name('^oracle') - - def manual(self, service, plugin_was_run): - service.add_manual_command('Brute-force SIDs using Nmap:', 'nmap {nmap_extra} -sV -p {port} --script="banner,oracle-sid-brute" -oN "{scandir}/{protocol}_{port}_oracle_sid-brute_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_sid-brute_nmap.xml" {address}') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(oracle* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_oracle_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_oracle_nmap.xml" {address}') - -class OracleTNScmd(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Oracle TNScmd" - self.tags = ['default', 'safe', 'databases'] - - def configure(self): - self.match_service_name('^oracle') - - def check(self): - if which('tnscmd10g') is None: - error('The tnscmd10g program could not be found. Make sure it is installed. (On Kali, run: sudo apt install tnscmd10g)') - - async def run(self, service): - if service.target.ipversion == 'IPv4': - await service.execute('tnscmd10g ping -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_ping.txt') - await service.execute('tnscmd10g version -h {address} -p {port} 2>&1', outfile='{protocol}_{port}_oracle_tnscmd_version.txt') - -class OracleScanner(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Oracle Scanner" - self.tags = ['default', 'safe', 'databases'] - - def configure(self): - self.match_service_name('^oracle') - - def check(self): - if which('oscanner') is None: - error('The oscanner program could not be found. Make sure it is installed. (On Kali, run: sudo apt install oscanner)') - - async def run(self, service): - await service.execute('oscanner -v -s {address} -P {port} 2>&1', outfile='{protocol}_{port}_oracle_scanner.txt') - -class OracleODAT(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Oracle ODAT" - self.tags = ['default', 'safe', 'databases'] - - def configure(self): - self.match_service_name('^oracle') - - def manual(self, service, plugin_was_run): - service.add_manual_commands('Install ODAT (https://github.com/quentinhardy/odat) and run the following commands:', [ - 'python odat.py tnscmd -s {address} -p {port} --ping', - 'python odat.py tnscmd -s {address} -p {port} --version', - 'python odat.py tnscmd -s {address} -p {port} --status', - 'python odat.py sidguesser -s {address} -p {port}', - 'python odat.py passwordguesser -s {address} -p {port} -d --accounts-file accounts/accounts_multiple.txt', - 'python odat.py tnspoison -s {address} -p {port} -d --test-module' - ]) - -class OraclePatator(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Oracle Patator" - self.tags = ['default', 'databases'] - - def configure(self): - self.match_service_name('^oracle') - - def manual(self, service, plugin_was_run): - service.add_manual_command('Install Oracle Instant Client (https://github.com/rapid7/metasploit-framework/wiki/How-to-get-Oracle-Support-working-with-Kali-Linux) and then bruteforce with patator:', 'patator oracle_login host={address} port={port} user=COMBO00 password=COMBO01 0=/usr/share/seclists/Passwords/Default-Credentials/oracle-betterdefaultpasslist.txt -x ignore:code=ORA-01017 -x ignore:code=ORA-28000') diff --git a/plugins/default-port-scan.py b/plugins/default-port-scan.py deleted file mode 100644 index 8a4036d..0000000 --- a/plugins/default-port-scan.py +++ /dev/null @@ -1,103 +0,0 @@ -from autorecon.plugins import PortScan -from autorecon.io import info, error -from autorecon.config import config -import os, re - -class QuickTCPPortScan(PortScan): - - def __init__(self): - super().__init__() - self.name = 'Top TCP Ports' - self.description = 'Performs an Nmap scan of the top 1000 TCP ports.' - self.type = 'tcp' - self.tags = ['default', 'default-port-scan'] - self.priority = 0 - - async def run(self, target): - if target.ports: # Don't run this plugin if there are custom ports. - return [] - - if config['proxychains']: - traceroute_os = '' - else: - traceroute_os = ' -A --osscan-guess' - - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) - services = await target.extract_services(stdout) - await process.wait() - return services - -class AllTCPPortScan(PortScan): - - def __init__(self): - super().__init__() - self.name = 'All TCP Ports' - self.description = 'Performs an Nmap scan of all TCP ports.' - self.type = 'tcp' - self.specific_ports = True - self.tags = ['default', 'default-port-scan', 'long'] - - async def run(self, target): - if config['proxychains']: - traceroute_os = '' - else: - traceroute_os = ' -A --osscan-guess' - - if target.ports: - if target.ports['tcp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p ' + target.ports['tcp'] + ' -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) - else: - return [] - else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sV -sC --version-all' + traceroute_os + ' -p- -oN "{scandir}/_full_tcp_nmap.txt" -oX "{scandir}/xml/_full_tcp_nmap.xml" {address}', blocking=False) - services = [] - while True: - line = await stdout.readline() - if line is not None: - match = re.search('^Discovered open port ([0-9]+)/tcp', line) - if match: - info('Discovered open port {bmagenta}tcp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) - service = target.extract_service(line) - if service: - services.append(service) - else: - break - await process.wait() - return services - -class Top100UDPPortScan(PortScan): - - def __init__(self): - super().__init__() - self.name = 'Top 100 UDP Ports' - self.description = 'Performs an Nmap scan of the top 100 UDP ports.' - self.type = 'udp' - self.specific_ports = True - self.tags = ['default', 'default-port-scan', 'long'] - - async def run(self, target): - # Only run UDP scan if user is root. - if os.getuid() == 0: - if target.ports: - if target.ports['udp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --osscan-guess -p ' + target.ports['udp'] + ' -oN "{scandir}/_custom_ports_udp_nmap.txt" -oX "{scandir}/xml/_custom_ports_udp_nmap.xml" {address}', blocking=False) - else: - return [] - else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -sU -A --top-ports 100 -oN "{scandir}/_top_100_udp_nmap.txt" -oX "{scandir}/xml/_top_100_udp_nmap.xml" {address}', blocking=False) - services = [] - while True: - line = await stdout.readline() - if line is not None: - match = re.search('^Discovered open port ([0-9]+)/udp', line) - if match: - info('Discovered open port {bmagenta}udp/' + match.group(1) + '{rst} on {byellow}' + target.address + '{rst}', verbosity=1) - service = target.extract_service(line) - if service: - services.append(service) - else: - break - await process.wait() - return services - else: - error('UDP scan requires AutoRecon be run with root privileges.') diff --git a/plugins/dns.py b/plugins/dns.py deleted file mode 100644 index 8793b3b..0000000 --- a/plugins/dns.py +++ /dev/null @@ -1,57 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapDNS(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap DNS' - self.tags = ['default', 'safe', 'dns'] - - def configure(self): - self.match_service_name('^domain') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_dns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_dns_nmap.xml" {address}') - -class DNSZoneTransfer(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'DNS Zone Transfer' - self.tags = ['default', 'safe', 'dns'] - - def configure(self): - self.match_service_name('^domain') - - async def run(self, service): - if self.get_global('domain'): - await service.execute('dig AXFR -p {port} @{address} ' + self.get_global('domain'), outfile='{protocol}_{port}_dns_zone-transfer-domain.txt') - if service.target.type == 'hostname': - await service.execute('dig AXFR -p {port} @{address} {address}', outfile='{protocol}_{port}_dns_zone-transfer-hostname.txt') - await service.execute('dig AXFR -p {port} @{address}', outfile='{protocol}_{port}_dns_zone-transfer.txt') - -class DNSReverseLookup(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'DNS Reverse Lookup' - self.tags = ['default', 'safe', 'dns'] - - def configure(self): - self.match_service_name('^domain') - - async def run(self, service): - await service.execute('dig -p {port} -x {address} @{address}', outfile='{protocol}_{port}_dns_reverse-lookup.txt') - -class NmapMulticastDNS(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap Multicast DNS' - self.tags = ['default', 'safe', 'dns'] - - def configure(self): - self.match_service_name(['^mdns', '^zeroconf']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(dns* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_multicastdns_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_multicastdns_nmap.xml" {address}') diff --git a/plugins/ftp.py b/plugins/ftp.py deleted file mode 100644 index edd470b..0000000 --- a/plugins/ftp.py +++ /dev/null @@ -1,30 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapFTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap FTP' - self.tags = ['default', 'safe', 'ftp'] - - def configure(self): - self.match_service_name(['^ftp', '^ftp\-data']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ftp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ftp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ftp_nmap.xml" {address}') - -class BruteforceFTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Bruteforce FTP" - self.tags = ['default', 'ftp'] - - def configure(self): - self.match_service_name(['^ftp', '^ftp\-data']) - - def manual(self, service, plugin_was_run): - service.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ftp_hydra.txt" ftp://{addressv6}', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ftp_medusa.txt" -M ftp -h {addressv6}' - ]) diff --git a/plugins/guess-port-scan.py b/plugins/guess-port-scan.py deleted file mode 100644 index 6ae5fa9..0000000 --- a/plugins/guess-port-scan.py +++ /dev/null @@ -1,48 +0,0 @@ -from autorecon.plugins import PortScan -from autorecon.targets import Service -import re - -class GuesPortScan(PortScan): - - def __init__(self): - super().__init__() - self.name = 'Guess TCP Ports' - self.type = 'tcp' - self.description = 'Performs an Nmap scan of the all TCP ports but guesses services based off the port found. Can be quicker. Proper service matching is performed at the end of the scan.' - self.tags = ['guess-port-scan', 'long'] - self.priority = 0 - - async def run(self, target): - if target.ports: - if target.ports['tcp']: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p ' + target.ports['tcp'] + ' -oN "{scandir}/_custom_ports_tcp_nmap.txt" -oX "{scandir}/xml/_custom_ports_tcp_nmap.xml" {address}', blocking=False) - else: - return [] - else: - process, stdout, stderr = await target.execute('nmap {nmap_extra} -A --osscan-guess --version-all -p- -oN "{scandir}/_quick_tcp_nmap.txt" -oX "{scandir}/xml/_quick_tcp_nmap.xml" {address}', blocking=False) - - insecure_ports = { - '20':'ftp', '21':'ftp', '22':'ssh', '23':'telnet', '25':'smtp', '53':'domain', '69':'tftp', '79':'finger', '80':'http', '88':'kerberos', '109':'pop3', '110':'pop3', '111':'rpcbind', '119':'nntp', '135':'msrpc', '139':'netbios-ssn', '143':'imap', '161':'snmp', '220':'imap', '389':'ldap', '433':'nntp', '445':'smb', '587':'smtp', '631':'ipp', '873':'rsync', '1098':'java-rmi', '1099':'java-rmi', '1433':'mssql', '1521':'oracle', '2049':'nfs', '2483':'oracle', '3020':'smb', '3306':'mysql', '3389':'rdp', '3632':'distccd', '5060':'asterisk', '5500':'vnc', '5900':'vnc', '5985':'wsman', '6379':'redis', '8080':'http-proxy', '27017':'mongod', '27018':'mongod', '27019':'mongod' - } - secure_ports = { - '443':'https', '465':'smtp', '563':'nntp', '585':'imaps', '593':'msrpc', '636':'ldap', '989':'ftp', '990':'ftp', '992':'telnet', '993':'imaps', '995':'pop3s', '2484':'oracle', '5061':'asterisk', '5986':'wsman' - } - - services = [] - while True: - line = await stdout.readline() - if line is not None: - match = re.match('^Discovered open port ([0-9]+)/tcp', line) - if match: - if match.group(1) in insecure_ports.keys(): - await target.add_service(Service('tcp', match.group(1), insecure_ports[match.group(1)])) - elif match.group(1) in secure_ports.keys(): - await target.add_service(Service('tcp', match.group(1), secure_ports[match.group(1)], True)) - service = target.extract_service(line) - if service is not None: - services.append(service) - else: - break - - await process.wait() - return services diff --git a/plugins/http_server.py b/plugins/http_server.py deleted file mode 100644 index 97bcc1f..0000000 --- a/plugins/http_server.py +++ /dev/null @@ -1,214 +0,0 @@ -from autorecon.plugins import ServiceScan -from autorecon.io import error, info, fformat -from shutil import which -import os - -class NmapHTTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap HTTP" - self.tags = ['default', 'safe', 'http'] - - def configure(self): - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - self.add_pattern('Server: ([^\n]+)', description='Identified HTTP Server: {match}') - self.add_pattern('WebDAV is ENABLED', description='WebDAV is enabled') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(http* or ssl*) and not (brute or broadcast or dos or external or http-slowloris* or fuzzer)" -oN "{scandir}/{protocol}_{port}_{http_scheme}_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_{http_scheme}_nmap.xml" {address}') - -class BruteforceHTTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Bruteforce HTTP" - self.tags = ['default', 'http'] - - def configure(self): - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - - def manual(self, service, plugin_was_run): - service.add_manual_commands('Credential bruteforcing commands (don\'t run these without modifying them):', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_auth_hydra.txt" {http_scheme}-get://{addressv6}/path/to/auth/area', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_auth_medusa.txt" -M http -h {addressv6} -m DIR:/path/to/auth/area', - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_{http_scheme}_form_hydra.txt" {http_scheme}-post-form://{addressv6}/path/to/login.php:username=^USER^&password=^PASS^:invalid-login-message', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_{http_scheme}_form_medusa.txt" -M web-form -h {addressv6} -m FORM:/path/to/login.php -m FORM-DATA:"post?username=&password=" -m DENY-SIGNAL:"invalid login message"' - ]) - -class Curl(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Curl" - self.tags = ['default', 'safe', 'http'] - - def configure(self): - self.add_option("path", default="/", help="The path on the web server to curl. Default: %(default)s") - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - self.add_pattern('(?i)powered[ -]by[^\n]+') - - async def run(self, service): - if service.protocol == 'tcp': - await service.execute('curl -sSik {http_scheme}://{addressv6}:{port}' + self.get_option('path'), outfile='{protocol}_{port}_{http_scheme}_curl.html') - -class CurlRobots(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Curl Robots" - self.tags = ['default', 'safe', 'http'] - - def configure(self): - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - - async def run(self, service): - if service.protocol == 'tcp': - _, stdout, _ = await service.execute('curl -sSikf {http_scheme}://{addressv6}:{port}/robots.txt', future_outfile='{protocol}_{port}_{http_scheme}_curl-robots.txt') - lines = await stdout.readlines() - - if lines: - filename = fformat('{scandir}/{protocol}_{port}_{http_scheme}_curl-robots.txt') - with open(filename, mode='wt', encoding='utf8') as robots: - robots.write('\n'.join(lines)) - else: - info('{bblue}[' + fformat('{tag}') + ']{rst} There did not appear to be a robots.txt file in the webroot (/).') - -class DirBuster(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Directory Buster" - self.slug = 'dirbuster' - self.priority = 0 - self.tags = ['default', 'safe', 'long', 'http'] - - def configure(self): - self.add_choice_option('tool', default='feroxbuster', choices=['feroxbuster', 'gobuster', 'dirsearch', 'ffuf', 'dirb'], help='The tool to use for directory busting. Default: %(default)s') - self.add_list_option('wordlist', default=['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web-Content/big.txt', '/usr/share/seclists/Discovery/Web-Content/raft-large-words.txt'], help='The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: %(default)s') - self.add_option('threads', default=10, help='The number of threads to use when directory busting. Default: %(default)s') - self.add_option('ext', default='txt,html,php,asp,aspx,jsp', help='The extensions you wish to fuzz (no dot, comma separated). Default: %(default)s') - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - - def check(self): - tool = self.get_option('tool') - if tool == 'feroxbuster': - if which('feroxbuster') is None: - error('The feroxbuster program could not be found. Make sure it is installed. (On Kali, run: sudo apt install feroxbuster)') - elif tool == 'gobuster': - if which('gobuster') is None: - error('The gobuster program could not be found. Make sure it is installed. (On Kali, run: sudo apt install gobuster)') - elif tool == 'dirsearch': - if which('dirsearch') is None: - error('The dirsearch program could not be found. Make sure it is installed. (On Kali, run: sudo apt install dirsearch)') - - async def run(self, service): - dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) - for wordlist in self.get_option('wordlist'): - name = os.path.splitext(os.path.basename(wordlist))[0] - if self.get_option('tool') == 'feroxbuster': - await service.execute('feroxbuster -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -x "' + self.get_option('ext') + '" -v -k -n -q -o "{scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_' + name + '.txt"') - elif self.get_option('tool') == 'gobuster': - await service.execute('gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_' + name + '.txt"') - elif self.get_option('tool') == 'dirsearch': - if service.target.ipversion == 'IPv6': - error('dirsearch does not support IPv6.') - else: - await service.execute('dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -e "' + self.get_option('ext') + '" -f -q -w ' + wordlist + ' --format=plain -o "{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_' + name + '.txt"') - elif self.get_option('tool') == 'ffuf': - await service.execute('ffuf -u {http_scheme}://{addressv6}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w ' + wordlist + ' -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_' + name + '.txt') - elif self.get_option('tool') == 'dirb': - await service.execute('dirb {http_scheme}://{addressv6}:{port}/ ' + wordlist + ' -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_' + name + '.txt"') - - def manual(self, service, plugin_was_run): - dot_extensions = ','.join(['.' + x for x in self.get_option('ext').split(',')]) - if self.get_option('tool') == 'feroxbuster': - service.add_manual_command('(feroxbuster) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'feroxbuster -u {http_scheme}://{addressv6}:{port} -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x "' + self.get_option('ext') + '" -v -k -n -o {scandir}/{protocol}_{port}_{http_scheme}_feroxbuster_dirbuster.txt' - ]) - elif self.get_option('tool') == 'gobuster': - service.add_manual_command('(gobuster v3) Multi-threaded directory/file enumeration for web servers using various wordlists:', [ - 'gobuster dir -u {http_scheme}://{addressv6}:{port}/ -t ' + str(self.get_option('threads')) + ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -x "' + self.get_option('ext') + '" -z -o "{scandir}/{protocol}_{port}_{http_scheme}_gobuster_dirbuster.txt"' - ]) - elif self.get_option('tool') == 'dirsearch': - if service.target.ipversion == 'IPv4': - service.add_manual_command('(dirsearch) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirsearch -u {http_scheme}://{address}:{port}/ -t ' + str(self.get_option('threads')) + ' -r -e "' + self.get_option('ext') + '" -f -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --format=plain --output="{scandir}/{protocol}_{port}_{http_scheme}_dirsearch_dirbuster.txt"' - ]) - elif self.get_option('tool') == 'ffuf': - service.add_manual_command('(ffuf) Multi-threaded recursive directory/file enumeration for web servers using various wordlists:', [ - 'ffuf -u {http_scheme}://{addressv6}:{port}/FUZZ -t ' + str(self.get_option('threads')) + ' -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -e "' + dot_extensions + '" -v -noninteractive | tee {scandir}/{protocol}_{port}_{http_scheme}_ffuf_dirbuster.txt' - ]) - elif self.get_option('tool') == 'dirb': - service.add_manual_command('(dirb) Recursive directory/file enumeration for web servers using various wordlists:', [ - 'dirb {http_scheme}://{addressv6}:{port}/ /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -l -r -S -X ",' + dot_extensions + '" -o "{scandir}/{protocol}_{port}_{http_scheme}_dirb_dirbuster.txt"' - ]) - -class Nikto(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'nikto' - self.tags = ['default', 'safe', 'long', 'http'] - - def configure(self): - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - - def manual(self, service, plugin_was_run): - if service.target.ipversion == 'IPv4': - service.add_manual_command('(nikto) old but generally reliable web server enumeration tool:', 'nikto -ask=no -h {http_scheme}://{address}:{port} 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_nikto.txt"') - -class WhatWeb(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "whatweb" - self.tags = ['default', 'safe', 'http'] - - def configure(self): - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - - async def run(self, service): - if service.protocol == 'tcp' and service.target.ipversion == 'IPv4': - await service.execute('whatweb --color=never --no-errors -a 3 -v {http_scheme}://{address}:{port} 2>&1', outfile='{protocol}_{port}_{http_scheme}_whatweb.txt') - -class WkHTMLToImage(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "wkhtmltoimage" - self.tags = ['default', 'safe', 'http'] - - def configure(self): - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - - def check(self): - if which('wkhtmltoimage') is None: - error('The wkhtmltoimage program could not be found. Make sure it is installed. (On Kali, run: sudo apt install wkhtmltopdf)') - - async def run(self, service): - if which('wkhtmltoimage') is not None: - if service.protocol == 'tcp': - await service.execute('wkhtmltoimage --format png {http_scheme}://{addressv6}:{port}/ {scandir}/{protocol}_{port}_{http_scheme}_screenshot.png') - -class WPScan(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'WPScan' - self.tags = ['default', 'safe', 'http'] - - def configure(self): - self.match_service_name('^http') - self.match_service_name('^nacn_http$', negative_match=True) - - def manual(self, service, plugin_was_run): - service.add_manual_command('(wpscan) WordPress Security Scanner (useful if WordPress is found):', 'wpscan --url {http_scheme}://{addressv6}:{port}/ --no-update -e vp,vt,tt,cb,dbe,u,m --plugins-detection aggressive --plugins-version-detection aggressive -f cli-no-color 2>&1 | tee "{scandir}/{protocol}_{port}_{http_scheme}_wpscan.txt"') diff --git a/plugins/kerberos.py b/plugins/kerberos.py deleted file mode 100644 index 0830ff5..0000000 --- a/plugins/kerberos.py +++ /dev/null @@ -1,17 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapKerberos(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap Kerberos" - self.tags = ['default', 'safe', 'kerberos', 'active-directory'] - - def configure(self): - self.match_service_name(['^kerberos', '^kpasswd']) - - async def run(self, service): - if self.get_global('domain') and self.get_global('username-wordlist'): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" --script-args krb5-enum-users.realm="' + self.get_global('domain') + '",userdb="' + self.get_global('username-wordlist') + '" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') - else: - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,krb5-enum-users" -oN "{scandir}/{protocol}_{port}_kerberos_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_kerberos_nmap.xml" {address}') diff --git a/plugins/ldap.py b/plugins/ldap.py deleted file mode 100644 index 8f01f4f..0000000 --- a/plugins/ldap.py +++ /dev/null @@ -1,29 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapLDAP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap LDAP" - self.tags = ['default', 'safe', 'ldap', 'active-directory'] - - def configure(self): - self.match_service_name('^ldap') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(ldap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_ldap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ldap_nmap.xml" {address}') - -class LDAPSearch(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'LDAP Search' - self.tags = ['default', 'safe', 'ldap', 'active-directory'] - - def configure(self): - self.match_service_name('^ldap') - - def manual(self, service, plugin_was_run): - service.add_manual_command('ldapsearch command (modify before running):', [ - 'ldapsearch -x -D "" -w """ -p {port} -h {address} -b "dc=example,dc=com" -s sub "(objectclass=*) 2>&1 | tee > "{scandir}/{protocol}_{port}_ldap_all-entries.txt"' - ]) diff --git a/plugins/misc.py b/plugins/misc.py deleted file mode 100644 index 4775909..0000000 --- a/plugins/misc.py +++ /dev/null @@ -1,188 +0,0 @@ -from autorecon.plugins import ServiceScan -from autorecon.io import fformat - -class NmapCassandra(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap Cassandra" - self.tags = ['default', 'safe', 'cassandra'] - - def configure(self): - self.match_service_name('^apani1') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cassandra* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cassandra_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cassandra_nmap.xml" {address}') - -class NmapCUPS(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap CUPS" - self.tags = ['default', 'safe', 'cups'] - - def configure(self): - self.match_service_name('^ipp') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(cups* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_cups_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_cups_nmap.xml" {address}') - -class NmapDistccd(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap distccd" - self.tags = ['default', 'safe', 'distccd'] - - def configure(self): - self.match_service_name('^distccd') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,distcc-cve2004-2687" --script-args="distcc-cve2004-2687.cmd=id" -oN "{scandir}/{protocol}_{port}_distcc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_distcc_nmap.xml" {address}') - -class NmapFinger(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap finger" - self.tags = ['default', 'safe', 'finger'] - - def configure(self): - self.match_service_name('^finger') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,finger" -oN "{scandir}/{protocol}_{port}_finger_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_finger_nmap.xml" {address}') - -class NmapIMAP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap IMAP" - self.tags = ['default', 'safe', 'imap', 'email'] - - def configure(self): - self.match_service_name('^imap') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(imap* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_imap_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_imap_nmap.xml" {address}') - -class NmapIrc(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap IRC' - self.tags = ['default', 'safe', 'irc'] - - def configure(self): - self.match_service_name('^irc') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV --script irc-botnet-channels,irc-info,irc-unrealircd-backdoor -oN "{scandir}/{protocol}_{port}_irc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_irc_nmap.xml" -p {port} {address}') - -class NmapNNTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap NNTP" - self.tags = ['default', 'safe', 'nntp'] - - def configure(self): - self.match_service_name('^nntp') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nntp-ntlm-info" -oN "{scandir}/{protocol}_{port}_nntp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nntp_nmap.xml" {address}') - -class NmapPOP3(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap POP3" - self.tags = ['default', 'safe', 'pop3', 'email'] - - def configure(self): - self.match_service_name('^pop3') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(pop3* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_pop3_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_pop3_nmap.xml" {address}') - -class NmapRMI(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap RMI" - self.tags = ['default', 'safe', 'rmi'] - - def configure(self): - self.match_service_name(['^java\-rmi', '^rmiregistry']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,rmi-vuln-classloader,rmi-dumpregistry" -oN "{scandir}/{protocol}_{port}_rmi_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rmi_nmap.xml" {address}') - -class NmapTelnet(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap Telnet' - self.tags = ['default', 'safe', 'telnet'] - - def configure(self): - self.match_service_name('^telnet') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,telnet-encryption,telnet-ntlm-info" -oN "{scandir}/{protocol}_{port}_telnet-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_telnet_nmap.xml" {address}') - -class NmapTFTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap TFTP' - self.tags = ['default', 'safe', 'tftp'] - - def configure(self): - self.match_service_name('^tftp') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,tftp-enum" -oN "{scandir}/{protocol}_{port}_tftp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_tftp_nmap.xml" {address}') - -class NmapVNC(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap VNC' - self.tags = ['default', 'safe', 'vnc'] - - def configure(self): - self.match_service_name('^vnc') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(vnc* or realvnc* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_vnc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_vnc_nmap.xml" {address}') - -class WinRMDetection(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'WinRM Detection' - self.tags = ['default', 'safe', 'winrm'] - - def configure(self): - self.match_service_name('^wsman') - self.match_service('tcp', [5985, 5986], '^http') - - async def run(self, service): - filename = fformat('{scandir}/{protocol}_{port}_winrm-detection.txt') - with open(filename, mode='wt', encoding='utf8') as winrm: - winrm.write('WinRM was possibly detected running on ' + service.protocol + ' port ' + str(service.port) + '.\nCheck _manual_commands.txt for manual commands you can run against this service.') - - def manual(self, service, plugin_was_run): - service.add_manual_commands('Bruteforce logins:', [ - 'crackmapexec winrm {address} -d ' + self.get_global('domain', default='') + ' -u ' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + ' -p ' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') - ]) - - service.add_manual_commands('Check login (requires credentials):', [ - 'crackmapexec winrm {address} -d ' + self.get_global('domain', default='') + ' -u -p -x "whoami"' - ]) - - service.add_manual_commands('Evil WinRM (gem install evil-winrm):', [ - 'evil-winrm -u -p -i {address}', - 'evil-winrm -u -H -i {address}' - ]) diff --git a/plugins/nfs.py b/plugins/nfs.py deleted file mode 100644 index 515a2ec..0000000 --- a/plugins/nfs.py +++ /dev/null @@ -1,40 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapNFS(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap NFS" - self.tags = ['default', 'safe', 'nfs'] - - def configure(self): - self.match_service_name(['^nfs', '^rpcbind']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rpcinfo or nfs*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_nfs_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_nfs_nmap.xml" {address}') - -class Showmount(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "showmount" - self.tags = ['default', 'safe', 'nfs'] - - def configure(self): - self.match_service_name(['^nfs', '^rpcbind']) - - async def run(self, service): - await service.execute('showmount -e {address} 2>&1', outfile='{protocol}_{port}_showmount.txt') - -class NmapMountd(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap Mountd" - self.tags = ['default', 'safe', 'nfs'] - - def configure(self): - self.match_service_name('^mountd') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,nfs* and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_mountd_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_mountd_nmap.xml" {address}') diff --git a/plugins/rdp.py b/plugins/rdp.py deleted file mode 100644 index e99334f..0000000 --- a/plugins/rdp.py +++ /dev/null @@ -1,30 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapRDP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap RDP" - self.tags = ['default', 'safe', 'rdp'] - - def configure(self): - self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rdp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rdp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rdp_nmap.xml" {address}') - -class BruteforceRDP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Bruteforce RDP" - self.tags = ['default', 'rdp'] - - def configure(self): - self.match_service_name(['^rdp', '^ms\-wbt\-server', '^ms\-term\-serv']) - - def manual(self, service, plugin_was_run): - service.add_manual_commands('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_rdp_hydra.txt" rdp://{addressv6}', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_rdp_medusa.txt" -M rdp -h {addressv6}' - ]) diff --git a/plugins/redis.py b/plugins/redis.py deleted file mode 100644 index 4603904..0000000 --- a/plugins/redis.py +++ /dev/null @@ -1,37 +0,0 @@ -from autorecon.plugins import ServiceScan -from autorecon.io import error -from shutil import which - -class NmapRedis(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap Redis' - self.tags = ['default', 'safe', 'redis'] - - def configure(self): - self.match_service_name('^redis$') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,redis-info" -oN "{scandir}/{protocol}_{port}_redis_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_redis_nmap.xml" {address}') - -class RedisCli(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Redis Cli' - self.tags = ['default', 'safe', 'redis'] - - def configure(self): - self.match_service_name('^redis$') - - def check(self): - if which('redis-cli') is None: - error('The redis-cli program could not be found. Make sure it is installed. (On Kali, run: sudo apt install redis-tools)') - - async def run(self, service): - if which('redis-cli') is not None: - _, stdout, _ = await service.execute('redis-cli -p {port} -h {address} INFO', outfile='{protocol}_{port}_redis_info.txt') - if not (await stdout.readline()).startswith('NOAUTH Authentication required'): - await service.execute('redis-cli -p {port} -h {address} CONFIG GET \'*\'', outfile='{protocol}_{port}_redis_config.txt') - await service.execute('redis-cli -p {port} -h {address} CLIENT LIST', outfile='{protocol}_{port}_redis_client-list.txt') diff --git a/plugins/reporting.py b/plugins/reporting.py deleted file mode 100644 index e5b87be..0000000 --- a/plugins/reporting.py +++ /dev/null @@ -1,163 +0,0 @@ -from autorecon.plugins import Report -from autorecon.config import config -from xml.sax.saxutils import escape -import os, glob - -class CherryTree(Report): - - def __init__(self): - super().__init__() - self.name = 'CherryTree' - self.tags = [] - - async def run(self, targets): - if len(targets) > 1: - report = os.path.join(config['outdir'], 'report.xml.ctd') - elif len(targets) == 1: - report = os.path.join(targets[0].reportdir, 'report.xml.ctd') - else: - return - - with open(report, 'w') as output: - output.writelines('\n\n') - for target in targets: - output.writelines('\n') - - files = [os.path.abspath(filename) for filename in glob.iglob(os.path.join(target.scandir, '**/*'), recursive=True) if os.path.isfile(filename) and filename.endswith(('.txt', '.html'))] - - if target.scans['ports']: - output.writelines('\n') - for scan in target.scans['ports'].keys(): - if len(target.scans['ports'][scan]['commands']) > 0: - output.writelines('\n') - for command in target.scans['ports'][scan]['commands']: - output.writelines('' + escape(command[0])) - for filename in files: - if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): - output.writelines('\n\n' + escape(filename) + ':\n\n') - with open(filename, 'r') as file: - output.writelines(escape(file.read()) + '\n') - output.writelines('\n') - output.writelines('\n') - output.writelines('\n') - if target.scans['services']: - output.writelines('\n') - for service in target.scans['services'].keys(): - output.writelines('\n') - for plugin in target.scans['services'][service].keys(): - if len(target.scans['services'][service][plugin]['commands']) > 0: - output.writelines('\n') - for command in target.scans['services'][service][plugin]['commands']: - output.writelines('' + escape(command[0])) - for filename in files: - if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): - output.writelines('\n\n' + escape(filename) + ':\n\n') - with open(filename, 'r') as file: - output.writelines(escape(file.read()) + '\n') - output.writelines('\n') - output.writelines('\n') - output.writelines('\n') - output.writelines('\n') - - manual_commands = os.path.join(target.scandir, '_manual_commands.txt') - if os.path.isfile(manual_commands): - output.writelines('\n') - with open(manual_commands, 'r') as file: - output.writelines('' + escape(file.read()) + '\n') - output.writelines('\n') - - patterns = os.path.join(target.scandir, '_patterns.log') - if os.path.isfile(patterns): - output.writelines('\n') - with open(patterns, 'r') as file: - output.writelines('' + escape(file.read()) + '\n') - output.writelines('\n') - - commands = os.path.join(target.scandir, '_commands.log') - if os.path.isfile(commands): - output.writelines('\n') - with open(commands, 'r') as file: - output.writelines('' + escape(file.read()) + '\n') - output.writelines('\n') - - errors = os.path.join(target.scandir, '_errors.log') - if os.path.isfile(errors): - output.writelines('\n') - with open(errors, 'r') as file: - output.writelines('' + escape(file.read()) + '\n') - output.writelines('\n') - output.writelines('\n') - - output.writelines('') - -class Markdown(Report): - - def __init__(self): - super().__init__() - self.name = 'Markdown' - - async def run(self, targets): - if len(targets) > 1: - report = os.path.join(config['outdir'], 'report.md') - elif len(targets) == 1: - report = os.path.join(targets[0].reportdir, 'report.md') - else: - return - - os.makedirs(report, exist_ok=True) - - for target in targets: - os.makedirs(os.path.join(report, target.address), exist_ok=True) - - files = [os.path.abspath(filename) for filename in glob.iglob(os.path.join(target.scandir, '**/*'), recursive=True) if os.path.isfile(filename) and filename.endswith(('.txt', '.html'))] - - if target.scans['ports']: - os.makedirs(os.path.join(report, target.address, 'Port Scans'), exist_ok=True) - for scan in target.scans['ports'].keys(): - if len(target.scans['ports'][scan]['commands']) > 0: - with open(os.path.join(report, target.address, 'Port Scans', 'PortScan - ' + target.scans['ports'][scan]['plugin'].name + '.md'), 'w') as output: - for command in target.scans['ports'][scan]['commands']: - output.writelines('```bash\n' + command[0] + '\n```') - for filename in files: - if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): - output.writelines('\n\n[' + filename + '](file://' + filename + '):\n\n') - with open(filename, 'r') as file: - output.writelines('```\n' + file.read() + '\n```\n') - if target.scans['services']: - os.makedirs(os.path.join(report, target.address, 'Services'), exist_ok=True) - for service in target.scans['services'].keys(): - os.makedirs(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-')), exist_ok=True) - for plugin in target.scans['services'][service].keys(): - if len(target.scans['services'][service][plugin]['commands']) > 0: - with open(os.path.join(report, target.address, 'Services', 'Service - ' + service.tag().replace('/', '-'), target.scans['services'][service][plugin]['plugin'].name + '.md'), 'w') as output: - for command in target.scans['services'][service][plugin]['commands']: - output.writelines('```bash\n' + command[0] + '\n```') - for filename in files: - if filename in command[0] or (command[1] is not None and filename == command[1]) or (command[2] is not None and filename == command[2]): - output.writelines('\n\n[' + filename + '](file://' + filename + '):\n\n') - with open(filename, 'r') as file: - output.writelines('```\n' + file.read() + '\n```\n') - - manual_commands = os.path.join(target.scandir, '_manual_commands.txt') - if os.path.isfile(manual_commands): - with open(os.path.join(report, target.address, 'Manual Commands' + '.md'), 'w') as output: - with open(manual_commands, 'r') as file: - output.writelines('```bash\n' + file.read() + '\n```') - - patterns = os.path.join(target.scandir, '_patterns.log') - if os.path.isfile(patterns): - with open(os.path.join(report, target.address, 'Patterns' + '.md'), 'w') as output: - with open(patterns, 'r') as file: - output.writelines(file.read()) - - commands = os.path.join(target.scandir, '_commands.log') - if os.path.isfile(commands): - with open(os.path.join(report, target.address, 'Commands' + '.md'), 'w') as output: - with open(commands, 'r') as file: - output.writelines('```bash\n' + file.read() + '\n```') - - errors = os.path.join(target.scandir, '_errors.log') - if os.path.isfile(errors): - with open(os.path.join(report, target.address, 'Errors' + '.md'), 'w') as output: - with open(errors, 'r') as file: - output.writelines('```\n' + file.read() + '\n```') diff --git a/plugins/rpc.py b/plugins/rpc.py deleted file mode 100644 index 429c499..0000000 --- a/plugins/rpc.py +++ /dev/null @@ -1,42 +0,0 @@ -from autorecon.plugins import ServiceScan -from autorecon.io import error, warn - -class NmapRPC(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap MSRPC" - self.tags = ['default', 'rpc'] - - def configure(self): - self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,msrpc-enum,rpc-grind,rpcinfo" -oN "{scandir}/{protocol}_{port}_rpc_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rpc_nmap.xml" {address}') - -class RPCClient(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "rpcclient" - self.tags = ['default', 'safe', 'rpc'] - - def configure(self): - self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) - - def manual(self, service, plugin_was_run): - service.add_manual_command('RPC Client:', 'rpcclient -p {port} -U "" {address}') - -class RPCDump(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'rpcdump' - self.tags = ['default', 'safe', 'rpc'] - - def configure(self): - self.match_service_name(['^msrpc', '^rpcbind', '^erpc']) - - async def run(self, service): - if service.protocol == 'tcp': - await service.execute('impacket-rpcdump -port {port} {address}', outfile='{protocol}_{port}_rpc_rpcdump.txt') diff --git a/plugins/rsync.py b/plugins/rsync.py deleted file mode 100644 index da9a4be..0000000 --- a/plugins/rsync.py +++ /dev/null @@ -1,27 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapRsync(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Nmap Rsync' - self.tags = ['default', 'safe', 'rsync'] - - def configure(self): - self.match_service_name('^rsync') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(rsync* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_rsync_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_rsync_nmap.xml" {address}') - -class RsyncList(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'Rsync List Files' - self.tags = ['default', 'safe', 'rsync'] - - def configure(self): - self.match_service_name('^rsync') - - async def run(self, service): - await service.execute('rsync -av --list-only rsync://{addressv6}:{port}', outfile='{protocol}_{port}_rsync_file_list.txt') diff --git a/plugins/sip.py b/plugins/sip.py deleted file mode 100644 index 8b60f7e..0000000 --- a/plugins/sip.py +++ /dev/null @@ -1,28 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapSIP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap SIP" - self.tags = ['default', 'safe', 'sip'] - - def configure(self): - self.match_service_name('^asterisk') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,sip-enum-users,sip-methods" -oN "{scandir}/{protocol}_{port}_sip_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_sip_nmap.xml" {address}') - -class SIPVicious(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "SIPVicious" - self.tags = ['default', 'safe', 'sip'] - - def configure(self): - self.match_service_name('^asterisk') - - def manual(self, service, plugin_was_run): - if service.target.ipversion == 'IPv4': - service.add_manual_command('svwar:', 'svwar -D -m INVITE -p {port} {address}') diff --git a/plugins/smb.py b/plugins/smb.py deleted file mode 100644 index e5319ab..0000000 --- a/plugins/smb.py +++ /dev/null @@ -1,104 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapSMB(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap SMB" - self.tags = ['default', 'safe', 'smb', 'active-directory'] - - def configure(self): - self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(nbstat or smb* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smb_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_nmap.xml" {address}') - -class SMBVuln(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "SMB Vulnerabilities" - self.tags = ['unsafe', 'smb', 'active-directory'] - - def configure(self): - self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}') - await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}') - await service.execute('nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}') - - def manual(self, service, plugin_was_run): - if not plugin_was_run: # Only suggest these if they weren't run. - service.add_manual_commands('Nmap scans for SMB vulnerabilities that could potentially cause a DoS if scanned (according to Nmap). Be careful:', [ - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms06-025" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms06-025.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms06-025.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms07-029" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms07-029.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms07-029.xml" {address}', - 'nmap {nmap_extra} -sV -p {port} --script="smb-vuln-ms08-067" --script-args="unsafe=1" -oN "{scandir}/{protocol}_{port}_smb_ms08-067.txt" -oX "{scandir}/xml/{protocol}_{port}_smb_ms08-067.xml" {address}' - ]) - -class Enum4Linux(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Enum4Linux" - self.tags = ['default', 'safe', 'active-directory'] - - def configure(self): - self.match_service_name(['^ldap', '^smb', '^microsoft\-ds', '^netbios']) - self.match_port('tcp', [139, 389, 445]) - self.match_port('udp', 137) - self.run_once(True) - - async def run(self, service): - if service.target.ipversion == 'IPv4': - await service.execute('enum4linux -a -M -l -d {address} 2>&1', outfile='enum4linux.txt') - -class NBTScan(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "nbtscan" - self.tags = ['default', 'safe', 'netbios', 'active-directory'] - - def configure(self): - self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - self.match_port('udp', 137) - self.run_once(True) - - async def run(self, service): - if service.target.ipversion == 'IPv4': - await service.execute('nbtscan -rvh {address} 2>&1', outfile='nbtscan.txt') - -class SMBClient(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "SMBClient" - self.tags = ['default', 'safe', 'smb', 'active-directory'] - - def configure(self): - self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - self.match_port('tcp', [139, 445]) - self.run_once(True) - - async def run(self, service): - await service.execute('smbclient -L\\\\ -N -I {address} 2>&1', outfile='smbclient.txt') - -class SMBMap(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "SMBMap" - self.tags = ['default', 'safe', 'smb', 'active-directory'] - - def configure(self): - self.match_service_name(['^smb', '^microsoft\-ds', '^netbios']) - - async def run(self, service): - if service.target.ipversion == 'IPv4': - await service.execute('smbmap -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} 2>&1', outfile='smbmap-share-permissions.txt') - await service.execute('smbmap -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} -R 2>&1', outfile='smbmap-list-contents.txt') - await service.execute('smbmap -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') - await service.execute('smbmap -u null -p "" -H {address} -P {port} -x "ipconfig /all" 2>&1', outfile='smbmap-execute-command.txt') diff --git a/plugins/smtp.py b/plugins/smtp.py deleted file mode 100644 index 705c154..0000000 --- a/plugins/smtp.py +++ /dev/null @@ -1,33 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapSMTP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap SMTP" - self.tags = ['default', 'safe', 'smtp', 'email'] - - def configure(self): - self.match_service_name('^smtp') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(smtp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_smtp_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_smtp_nmap.xml" {address}') - -class SMTPUserEnum(ServiceScan): - - def __init__(self): - super().__init__() - self.name = 'SMTP-User-Enum' - self.tags = ['default', 'safe', 'smtp', 'email'] - - def configure(self): - self.match_service_name('^smtp') - - async def run(self, service): - await service.execute('hydra smtp-enum://{addressv6}:{port}/vrfy -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_vrfy.txt') - await service.execute('hydra smtp-enum://{addressv6}:{port}/expn -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" 2>&1', outfile='{protocol}_{port}_smtp_user-enum_hydra_expn.txt') - - def manual(self, service, plugin_was_run): - service.add_manual_command('Try User Enumeration using "RCPT TO". Replace with the target\'s domain name:', [ - 'hydra smtp-enum://{addressv6}:{port}/rcpt -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -o "{scandir}/{protocol}_{port}_smtp_user-enum_hydra_rcpt.txt" -p ' - ]) diff --git a/plugins/snmp.py b/plugins/snmp.py deleted file mode 100644 index 7b104a0..0000000 --- a/plugins/snmp.py +++ /dev/null @@ -1,53 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapSNMP(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap SNMP" - self.tags = ['default', 'safe', 'snmp'] - - def configure(self): - self.match_service_name('^snmp') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,(snmp* or ssl*) and not (brute or broadcast or dos or external or fuzzer)" -oN "{scandir}/{protocol}_{port}_snmp-nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_snmp_nmap.xml" {address}') - -class OneSixtyOne(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "OneSixtyOne" - self.tags = ['default', 'safe', 'snmp'] - - def configure(self): - self.match_service_name('^snmp') - self.match_port('udp', 161) - self.run_once(True) - self.add_option('community-strings', default='/usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt', help='The file containing a list of community strings to try. Default: %(default)s') - - async def run(self, service): - if service.target.ipversion == 'IPv4': - await service.execute('onesixtyone -c ' + service.get_option('community-strings') + ' -dd {address} 2>&1', outfile='{protocol}_{port}_snmp_onesixtyone.txt') - -class SNMPWalk(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "SNMPWalk" - self.tags = ['default', 'safe', 'snmp'] - - def configure(self): - self.match_service_name('^snmp') - self.match_port('udp', 161) - self.run_once(True) - - async def run(self, service): - await service.execute('snmpwalk -c public -v 1 {address} 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.1.6.0 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_system_processes.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.2 2>&1', outfile='{scandir}/{protocol}_{port}_snmp_snmpwalk_running_processes.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.4.2.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_process_paths.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_storage_units.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.25.2.3.1.4 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_software_names.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.4.1.77.1.2.25 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_user_accounts.txt') - await service.execute('snmpwalk -c public -v 1 {address} 1.3.6.1.2.1.6.13.1.3 2>&1', outfile='{protocol}_{port}_snmp_snmpwalk_tcp_ports.txt') diff --git a/plugins/ssh.py b/plugins/ssh.py deleted file mode 100644 index 2ecedab..0000000 --- a/plugins/ssh.py +++ /dev/null @@ -1,30 +0,0 @@ -from autorecon.plugins import ServiceScan - -class NmapSSH(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Nmap SSH" - self.tags = ['default', 'safe', 'ssh'] - - def configure(self): - self.match_service_name('^ssh') - - async def run(self, service): - await service.execute('nmap {nmap_extra} -sV -p {port} --script="banner,ssh2-enum-algos,ssh-hostkey,ssh-auth-methods" -oN "{scandir}/{protocol}_{port}_ssh_nmap.txt" -oX "{scandir}/xml/{protocol}_{port}_ssh_nmap.xml" {address}') - -class BruteforceSSH(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "Bruteforce SSH" - self.tags = ['default', 'ssh'] - - def configure(self): - self.match_service_name('ssh') - - def manual(self, service, plugin_was_run): - service.add_manual_command('Bruteforce logins:', [ - 'hydra -L "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e nsr -s {port} -o "{scandir}/{protocol}_{port}_ssh_hydra.txt" ssh://{addressv6}', - 'medusa -U "' + self.get_global('username_wordlist', default='/usr/share/seclists/Usernames/top-usernames-shortlist.txt') + '" -P "' + self.get_global('password_wordlist', default='/usr/share/seclists/Passwords/darkweb2017-top100.txt') + '" -e ns -n {port} -O "{scandir}/{protocol}_{port}_ssh_medusa.txt" -M ssh -h {addressv6}' - ]) diff --git a/plugins/sslscan.py b/plugins/sslscan.py deleted file mode 100644 index 43071ac..0000000 --- a/plugins/sslscan.py +++ /dev/null @@ -1,16 +0,0 @@ -from autorecon.plugins import ServiceScan - -class SSLScan(ServiceScan): - - def __init__(self): - super().__init__() - self.name = "SSL Scan" - self.tags = ['default', 'safe', 'ssl', 'tls'] - - def configure(self): - self.match_all_service_names(True) - self.require_ssl(True) - - async def run(self, service): - if service.protocol == 'tcp' and service.secure: - await service.execute('sslscan --show-certificate --no-colour {addressv6}:{port} 2>&1', outfile='{protocol}_{port}_sslscan.html') From 9adcaa70f9885540ea6f1b180a72df10fe7879a0 Mon Sep 17 00:00:00 2001 From: Tib3rius <48113936+Tib3rius@users.noreply.github.com> Date: Tue, 14 Sep 2021 18:39:52 -0400 Subject: [PATCH 95/95] Update README.md --- README.md | 137 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index b29e614..bad12c6 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,10 @@ > It's like bowling with bumpers. - [@ippsec](https://twitter.com/ippsec) -# Please Read Before Using - -**This is a public beta of AutoRecon version 2, which is effectively a complete rewrite of version 1. As such, there are no promises about stability, and you should expect bugs. During this beta, testers are encouraged to try out the new features, especially the new plugin functionality, and report bugs when they are found. Feedback on improvements and changes is also encouraged. There is no guarantee that the current plugin system "API" will be the same when version 2 is released.** - -**A wiki will be added to this repository to more fully explain the features in AutoRecon version 2.** - # AutoRecon AutoRecon is a multi-threaded network reconnaissance tool which performs automated enumeration of services. It is intended as a time-saving tool for use in CTFs and other penetration testing environments (e.g. OSCP). It may also be useful in real-world engagements. -The tool works by firstly performing port scans / service detection scans. From those initial results, the tool will launch further enumeration scans of those services using a number of different tools. For example, if HTTP is found, nikto will be launched (as well as many others). +The tool works by firstly performing port scans / service detection scans. From those initial results, the tool will launch further enumeration scans of those services using a number of different tools. For example, if HTTP is found, feroxbuster will be launched (as well as many others). Everything in the tool is highly configurable. The default configuration performs **no automated exploitation** to keep the tool in line with OSCP exam rules. If you wish to add automatic exploit tools to the configuration, you do so at your own risk. The author will not be held responsible for negative actions that result from the mis-use of this tool. @@ -41,6 +35,30 @@ AutoRecon was inspired by three tools which the author used during the OSCP labs - Python 3 - `python3-pip` +- `pipx` (optional, but recommended) + +### Python 3 + +If you don't have these installed, and are running Kali Linux, you can execute the following: + +```bash +$ sudo apt install python3 +$ sudo apt install python3-pip +``` + +### `pipx` + +Further, it's recommended you use `pipx` to manage your python packages; this installs each python package in it's own virtualenv, and makes it available in the global context, which avoids conflicting package dependencies and the resulting instability. To summarize the installation instructions: + +```bash +$ sudo apt install python3-venv +$ python3 -m pip install --user pipx +$ python3 -m pipx ensurepath +``` + +You will have to re-source your ~/.bashrc or ~/.zshrc file (or open a new tab) after running these commands in order to use pipx. + +Note that if you want to run AutoRecon via pipx using sudo, you'll have to install pipx and AutoRecon using sudo as well. ### Supporting packages @@ -85,20 +103,25 @@ $ sudo apt install seclists curl enum4linux feroxbuster impacket-scripts nbtscan Ensure you have all of the requirements installed as per the previous section. -Clone the repository and switch to the beta branch: +### Using `pipx` (recommended) + +If installing using pipx, you'll need to run the installation command as root or with sudo in order to be able to run autorecon using sudo: ```bash -$ git clone --branch beta https://github.com/Tib3rius/AutoRecon +$ pipx install git+https://github.com/Tib3rius/AutoRecon.git ``` -If you already had a copy of the repository, you can run the following from the main directory to get the beta code: +### Using `pip` + +If installing using pip, you'll need to run the installation command as root or with sudo in order to be able to run autorecon using sudo: ```bash -$ git pull -$ git checkout beta +$ sudo python3 -m pip install git+https://github.com/Tib3rius/AutoRecon.git ``` -From within the AutoRecon directory, install the dependencies: +### Manual + +If you'd prefer not to use `pip` or `pipx`, you can always still install and execute `autorecon.py` manually as a script. From within the AutoRecon directory, install the dependencies: ```bash $ python3 -m pip install -r requirements.txt @@ -117,66 +140,72 @@ See detailed usage options below. AutoRecon uses Python 3 specific functionality and does not support Python 2. ``` -usage: autorecon.py [-t TARGET_FILE] [-p PORTS] [-m MAX_SCANS] [-mp MAX_PORT_SCANS] [-c CONFIG_FILE] [-g GLOBAL_FILE] [--tags TAGS] - [--exclude-tags TAGS] [--port-scans PLUGINS] [--service-scans PLUGINS] [--reports PLUGINS] [--plugins-dir PLUGINS_DIR] - [--add-plugins-dir PLUGINS_DIR] [-l [TYPE]] [-o OUTDIR] [--single-target] [--only-scans-dir] [--create-port-dirs] - [--heartbeat HEARTBEAT] [--timeout TIMEOUT] [--target-timeout TARGET_TIMEOUT] [--nmap NMAP | --nmap-append NMAP_APPEND] - [--proxychains] [--disable-sanity-checks] [--disable-keyboard-control] [--force-services SERVICE [SERVICE ...]] - [--accessible] [-v] [--version] [--curl.path VALUE] [--dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb}] - [--dirbuster.wordlist VALUE [VALUE ...]] [--dirbuster.threads VALUE] [--dirbuster.ext VALUE] - [--onesixtyone.community-strings VALUE] [--global.username-wordlist VALUE] [--global.password-wordlist VALUE] - [--global.domain VALUE] [-h] - [targets ...] +usage: autorecon [-t TARGET_FILE] [-p PORTS] [-m MAX_SCANS] [-mp MAX_PORT_SCANS] [-c CONFIG_FILE] [-g GLOBAL_FILE] + [--tags TAGS] [--exclude-tags TAGS] [--port-scans PLUGINS] [--service-scans PLUGINS] + [--reports PLUGINS] [--plugins-dir PLUGINS_DIR] [--add-plugins-dir PLUGINS_DIR] [-l [TYPE]] [-o OUTDIR] + [--single-target] [--only-scans-dir] [--create-port-dirs] [--heartbeat HEARTBEAT] [--timeout TIMEOUT] + [--target-timeout TARGET_TIMEOUT] [--nmap NMAP | --nmap-append NMAP_APPEND] [--proxychains] + [--disable-sanity-checks] [--disable-keyboard-control] [--force-services SERVICE [SERVICE ...]] + [--accessible] [-v] [--version] [--curl.path VALUE] + [--dirbuster.tool {feroxbuster,gobuster,dirsearch,ffuf,dirb}] [--dirbuster.wordlist VALUE [VALUE ...]] + [--dirbuster.threads VALUE] [--dirbuster.ext VALUE] [--onesixtyone.community-strings VALUE] + [--global.username-wordlist VALUE] [--global.password-wordlist VALUE] [--global.domain VALUE] [-h] + [targets ...] Network reconnaissance tool to port scan and automatically enumerate services found on multiple targets. positional arguments: - targets IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. foo.bar) to scan. + targets IP addresses (e.g. 10.0.0.1), CIDR notation (e.g. 10.0.0.1/24), or resolvable hostnames (e.g. + foo.bar) to scan. optional arguments: -t TARGET_FILE, --targets TARGET_FILE Read targets from file. -p PORTS, --ports PORTS - Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list with T:/U: To scan both - TCP/UDP, put port(s) at start or specify B: e.g. 53,T:21-25,80,U:123,B:123. Default: None + Comma separated list of ports / port ranges to scan. Specify TCP/UDP ports by prepending list + with T:/U: To scan both TCP/UDP, put port(s) at start or specify B: e.g. + 53,T:21-25,80,U:123,B:123. Default: None -m MAX_SCANS, --max-scans MAX_SCANS The maximum number of concurrent scans to run. Default: 50 -mp MAX_PORT_SCANS, --max-port-scans MAX_PORT_SCANS - The maximum number of concurrent port scans to run. Default: 10 (approx 20% of max-scans unless specified) + The maximum number of concurrent port scans to run. Default: 10 (approx 20% of max-scans unless + specified) -c CONFIG_FILE, --config CONFIG_FILE - Location of AutoRecon's config file. Default: /mnt/hgfs/AutoRecon/config.toml + Location of AutoRecon's config file. Default: /home/tib3rius/.config/AutoRecon/config.toml -g GLOBAL_FILE, --global-file GLOBAL_FILE - Location of AutoRecon's global file. Default: /mnt/hgfs/AutoRecon/global.toml - --tags TAGS Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group tags together. Separate - groups with a comma (,) to create multiple groups. For a plugin to be included, it must have all the tags specified in - at least one group. Default: default - --exclude-tags TAGS Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group tags together. Separate - groups with a comma (,) to create multiple groups. For a plugin to be excluded, it must have all the tags specified in - at least one group. Default: None - --port-scans PLUGINS Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: None + Location of AutoRecon's global file. Default: /home/tib3rius/.config/AutoRecon/global.toml + --tags TAGS Tags to determine which plugins should be included. Separate tags by a plus symbol (+) to group + tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be + included, it must have all the tags specified in at least one group. Default: default + --exclude-tags TAGS Tags to determine which plugins should be excluded. Separate tags by a plus symbol (+) to group + tags together. Separate groups with a comma (,) to create multiple groups. For a plugin to be + excluded, it must have all the tags specified in at least one group. Default: None + --port-scans PLUGINS Override --tags / --exclude-tags for the listed PortScan plugins (comma separated). Default: + None --service-scans PLUGINS - Override --tags / --exclude-tags for the listed ServiceScan plugins (comma separated). Default: None + Override --tags / --exclude-tags for the listed ServiceScan plugins (comma separated). Default: + None --reports PLUGINS Override --tags / --exclude-tags for the listed Report plugins (comma separated). Default: None --plugins-dir PLUGINS_DIR - The location of the plugins directory. Default: /mnt/hgfs/AutoRecon/plugins + The location of the plugins directory. Default: /home/tib3rius/.config/AutoRecon/plugins --add-plugins-dir PLUGINS_DIR The location of an additional plugins directory to add to the main one. Default: None -l [TYPE], --list [TYPE] List all plugins or plugins of a specific type. e.g. --list, --list port, --list service -o OUTDIR, --output OUTDIR The output directory for results. Default: results - --single-target Only scan a single target. A directory named after the target will not be created. Instead, the directory structure will - be created within the output directory. Default: False - --only-scans-dir Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) will not be created. - Default: False - --create-port-dirs Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and store results in these - directories. Default: True + --single-target Only scan a single target. A directory named after the target will not be created. Instead, the + directory structure will be created within the output directory. Default: False + --only-scans-dir Only create the "scans" directory for results. Other directories (e.g. exploit, loot, report) + will not be created. Default: False + --create-port-dirs Create directories for ports within the "scans" directory (e.g. scans/tcp80, scans/udp53) and + store results in these directories. Default: True --heartbeat HEARTBEAT Specifies the heartbeat interval (in seconds) for scan status messages. Default: 60 --timeout TIMEOUT Specifies the maximum amount of time in minutes that AutoRecon should run for. Default: None --target-timeout TARGET_TIMEOUT - Specifies the maximum amount of time in minutes that a target should be scanned for before abandoning it and moving on. - Default: None + Specifies the maximum amount of time in minutes that a target should be scanned for before + abandoning it and moving on. Default: None --nmap NMAP Override the {nmap_extra} variable in scans. Default: -vv --reason -Pn --nmap-append NMAP_APPEND Append to the default {nmap_extra} variable in scans. Default: -T4 @@ -200,23 +229,25 @@ plugin arguments: The tool to use for directory busting. Default: feroxbuster --dirbuster.wordlist VALUE [VALUE ...] The wordlist(s) to use when directory busting. Separate multiple wordlists with spaces. Default: - ['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web-Content/big.txt', - '/usr/share/seclists/Discovery/Web-Content/raft-large-words.txt'] + ['/usr/share/seclists/Discovery/Web-Content/common.txt', '/usr/share/seclists/Discovery/Web- + Content/big.txt', '/usr/share/seclists/Discovery/Web-Content/raft-large-words.txt'] --dirbuster.threads VALUE The number of threads to use when directory busting. Default: 10 --dirbuster.ext VALUE The extensions you wish to fuzz (no dot, comma separated). Default: txt,html,php,asp,aspx,jsp --onesixtyone.community-strings VALUE - The file containing a list of community strings to try. Default: /usr/share/seclists/Discovery/SNMP/common-snmp- - community-strings-onesixtyone.txt + The file containing a list of community strings to try. Default: + /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings-onesixtyone.txt global plugin arguments: These are optional arguments that can be used by all plugins. --global.username-wordlist VALUE - A wordlist of usernames, useful for bruteforcing. Default: /usr/share/seclists/Usernames/top-usernames-shortlist.txt + A wordlist of usernames, useful for bruteforcing. Default: /usr/share/seclists/Usernames/top- + usernames-shortlist.txt --global.password-wordlist VALUE - A wordlist of passwords, useful for bruteforcing. Default: /usr/share/seclists/Passwords/darkweb2017-top100.txt + A wordlist of passwords, useful for bruteforcing. Default: + /usr/share/seclists/Passwords/darkweb2017-top100.txt --global.domain VALUE The domain to use (if known). Used for DNS and/or Active Directory. Default: None ``` @@ -228,7 +259,7 @@ AutoRecon supports four levels of verbosity: * (none) Minimal output. AutoRecon will announce when scanning targets starts / ends. * (-v) Verbose output. AutoRecon will additionally announce when plugins start running, and report open ports and identified services. * (-vv) Very verbose output. AutoRecon will additionally specify the exact commands which are being run by plugins, highlight any patterns which are matched in command output, and announce when plugins end. -* (-vvv) Very very verbose output. AutoRecon will output everything. Literally every line from all commands which are currently running. When scanning multiple targets concurrently, this can lead to a ridiculous amount of output. It is not advised to use -vvv unless you absolutely need to see live output from commands. +* (-vvv) Very, very verbose output. AutoRecon will output everything. Literally every line from all commands which are currently running. When scanning multiple targets concurrently, this can lead to a ridiculous amount of output. It is not advised to use -vvv unless you absolutely need to see live output from commands. Note: You can change the verbosity of AutoRecon mid-scan by pressing the up and down arrow keys.