anubis-netsec 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anubis/API.py +5 -0
- anubis/__init__.py +1 -0
- anubis/cli.py +132 -0
- anubis/commands/__init__.py +1 -0
- anubis/commands/base.py +14 -0
- anubis/commands/target.py +174 -0
- anubis/scanners/__init__.py +0 -0
- anubis/scanners/anubis_db.py +29 -0
- anubis/scanners/censys.py +21 -0
- anubis/scanners/crt.py +132 -0
- anubis/scanners/dnssec.py +23 -0
- anubis/scanners/hackertarget.py +24 -0
- anubis/scanners/hudson_rock.py +24 -0
- anubis/scanners/netcraft.py +34 -0
- anubis/scanners/nmap.py +53 -0
- anubis/scanners/pkey.py +35 -0
- anubis/scanners/recursive.py +45 -0
- anubis/scanners/shodan.py +24 -0
- anubis/scanners/spyse.py +33 -0
- anubis/scanners/ssl.py +33 -0
- anubis/scanners/zonetransfer.py +41 -0
- anubis/utils/__init__.py +0 -0
- anubis/utils/color_print.py +27 -0
- anubis/utils/search_worker.py +68 -0
- anubis/utils/signal_handler.py +32 -0
- anubis_netsec-1.4.0.dist-info/METADATA +274 -0
- anubis_netsec-1.4.0.dist-info/RECORD +31 -0
- anubis_netsec-1.4.0.dist-info/WHEEL +5 -0
- anubis_netsec-1.4.0.dist-info/entry_points.txt +2 -0
- anubis_netsec-1.4.0.dist-info/licenses/LICENSE +21 -0
- anubis_netsec-1.4.0.dist-info/top_level.txt +1 -0
anubis/API.py
ADDED
anubis/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.4.0'
|
anubis/cli.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Usage:
|
|
3
|
+
anubis (-t TARGET | -f FILE) [-o FILENAME] [-bdinoprsSv] [-w SCAN] [-q NUM]
|
|
4
|
+
anubis -h
|
|
5
|
+
anubis (--version | -V)
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-h --help show this help message and exit
|
|
9
|
+
-t --target set target (comma separated, no spaces, if multiple)
|
|
10
|
+
-f --file set target (reads from file, one domain per line)
|
|
11
|
+
-n --with-nmap perform an nmap service/script scan
|
|
12
|
+
-o --output save to filename
|
|
13
|
+
-i --additional-info show additional information about the host from Shodan (requires API key)
|
|
14
|
+
-p --ip outputs the resolved IPs for each subdomain, and a full list of unique ips
|
|
15
|
+
-d --send-to-anubis-db send results to Anubis-DB
|
|
16
|
+
-r --recursive recursively search over all subdomains
|
|
17
|
+
-s --ssl run an ssl scan and output cipher + chain info
|
|
18
|
+
-S --silent only out put subdomains, one per line
|
|
19
|
+
-w --overwrite-nmap-scan SCAN overwrite default nmap scan (default -nPn -sV -sC)
|
|
20
|
+
-v --verbose print debug info and full request output
|
|
21
|
+
-q --queue-workers NUM override number of queue workers (default: 10, max: 100)
|
|
22
|
+
-V --version show version and exit
|
|
23
|
+
|
|
24
|
+
Help:
|
|
25
|
+
For help using this tool, please open an issue on the Github repository:
|
|
26
|
+
https://github.com/jonluca/anubis
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from functools import reduce
|
|
32
|
+
|
|
33
|
+
from docopt import docopt
|
|
34
|
+
|
|
35
|
+
import anubis.commands
|
|
36
|
+
# noinspection PyPep8Naming
|
|
37
|
+
from . import __version__ as VERSION
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Overload stdout to save output and change colors on filewrite
|
|
41
|
+
class StdOutHook:
|
|
42
|
+
lines = []
|
|
43
|
+
filename = ""
|
|
44
|
+
|
|
45
|
+
def __init__(self, filename, silent, output):
|
|
46
|
+
self.filename = filename
|
|
47
|
+
self.silent = silent
|
|
48
|
+
self.output = output
|
|
49
|
+
|
|
50
|
+
def write(self, text, override=False, **kwargs):
|
|
51
|
+
if not self.silent or override:
|
|
52
|
+
sys.__stdout__.write(text)
|
|
53
|
+
self.lines.append(text)
|
|
54
|
+
|
|
55
|
+
def write_out(self):
|
|
56
|
+
if self.output:
|
|
57
|
+
with open(self.filename, "w") as file:
|
|
58
|
+
for line in self.lines:
|
|
59
|
+
# remove stdout colors
|
|
60
|
+
line = line.replace('\033[91m', '')
|
|
61
|
+
line = line.replace('\033[92m', '')
|
|
62
|
+
line = line.replace('\033[93m', '')
|
|
63
|
+
line = line.replace('\033[94m', '')
|
|
64
|
+
line = line.replace('\033[95m', '')
|
|
65
|
+
line = line.replace('\033[0m', '')
|
|
66
|
+
file.write(line)
|
|
67
|
+
|
|
68
|
+
def flush(self):
|
|
69
|
+
# python3 compatability, does nothing
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# credit to https://stackoverflow.com/questions/1557571/how-do-i-get-time-of-a-python-programs-execution
|
|
74
|
+
def seconds_to_str(t):
|
|
75
|
+
return "%d:%02d:%02d.%03d" % reduce(lambda ll, b: divmod(ll[0], b) + ll[1:],
|
|
76
|
+
[(t * 1000,), 1000, 60, 60])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
try:
|
|
81
|
+
if sys.version_info < (3, 0):
|
|
82
|
+
sys.stdout.write("Sorry, requires Python 3.x, not Python 2.x\n")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
start_time = time.time()
|
|
86
|
+
|
|
87
|
+
options = docopt(__doc__, version=VERSION)
|
|
88
|
+
|
|
89
|
+
if options["--output"] or options['--silent']:
|
|
90
|
+
sys.stdout = StdOutHook(options["FILENAME"], options['--silent'],
|
|
91
|
+
options['--output'])
|
|
92
|
+
|
|
93
|
+
if options["--queue-workers"]:
|
|
94
|
+
if not options["--recursive"]:
|
|
95
|
+
print("Queue workers override only works with recursive option!")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
if int(options["--queue-workers"]) > 100:
|
|
98
|
+
print("Max queue worker override is 100!")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
if int(options["--queue-workers"]) < 1:
|
|
101
|
+
print("Queue workers can't be negative!")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
if not options["--target"] and not options['--file']:
|
|
105
|
+
print(
|
|
106
|
+
"Target required! Run with -h for usage instructions. Either -t target.host or -f file.txt required")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if options["--target"] and options['--file']:
|
|
110
|
+
print(
|
|
111
|
+
"Please only supply one target method - either read by file with -f or as an argument to -t, not both.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
print("""
|
|
115
|
+
d8888 888 d8b
|
|
116
|
+
d88888 888 Y8P
|
|
117
|
+
d88P888 888
|
|
118
|
+
d88P 888 88888b. 888 888 88888b. 888 .d8888b
|
|
119
|
+
d88P 888 888 "88b 888 888 888 "88b 888 88K
|
|
120
|
+
d88P 888 888 888 888 888 888 888 888 "Y8888b.
|
|
121
|
+
d8888888888 888 888 Y88b 888 888 d88P 888 X88
|
|
122
|
+
d88P 888 888 888 "Y88888 88888P" 888 88888P'
|
|
123
|
+
""")
|
|
124
|
+
|
|
125
|
+
command = anubis.commands.Target(options)
|
|
126
|
+
command.run()
|
|
127
|
+
print("Subdomain search took %s" % seconds_to_str(time.time() - start_time))
|
|
128
|
+
if options["--output"]:
|
|
129
|
+
sys.stdout.write_out()
|
|
130
|
+
except KeyboardInterrupt:
|
|
131
|
+
print("\nQuitting...")
|
|
132
|
+
sys.exit(0)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .target import *
|
anubis/commands/base.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""The base command."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Base(object):
|
|
5
|
+
"""A base command."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, options, *args, **kwargs):
|
|
8
|
+
self.options = options
|
|
9
|
+
self.args = args
|
|
10
|
+
self.kwargs = kwargs
|
|
11
|
+
|
|
12
|
+
def run(self):
|
|
13
|
+
raise NotImplementedError(
|
|
14
|
+
'run() method must be implemented by the overloading class')
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""The target command."""
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import socket
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
from anubis.scanners.anubis_db import search_anubisdb, send_to_anubisdb
|
|
10
|
+
from anubis.scanners.crt import search_crtsh
|
|
11
|
+
from anubis.scanners.hudson_rock import search_hudsonrock
|
|
12
|
+
from anubis.scanners.dnssec import dnssecc_subdomain_enum
|
|
13
|
+
from anubis.scanners.hackertarget import subdomain_hackertarget
|
|
14
|
+
from anubis.scanners.netcraft import search_netcraft
|
|
15
|
+
from anubis.scanners.nmap import scan_host
|
|
16
|
+
from anubis.scanners.recursive import recursive_search
|
|
17
|
+
from anubis.scanners.shodan import search_shodan
|
|
18
|
+
from anubis.scanners.spyse import search_spyse
|
|
19
|
+
from anubis.scanners.ssl import search_subject_alt_name
|
|
20
|
+
from anubis.scanners.zonetransfer import dns_zonetransfer
|
|
21
|
+
from anubis.utils.color_print import ColorPrint
|
|
22
|
+
from .base import Base
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Target(Base):
|
|
26
|
+
"""Main enumeration module"""
|
|
27
|
+
domains = list()
|
|
28
|
+
dedupe = set()
|
|
29
|
+
|
|
30
|
+
stdout = sys.stdout
|
|
31
|
+
|
|
32
|
+
def handle_exception(self, e, message=""):
|
|
33
|
+
if self.options["--verbose"]:
|
|
34
|
+
print(e)
|
|
35
|
+
if message:
|
|
36
|
+
ColorPrint.red(message)
|
|
37
|
+
|
|
38
|
+
def init(self):
|
|
39
|
+
if self.options["FILE"]:
|
|
40
|
+
full_path = os.path.join(os.getcwd(), self.options["FILE"])
|
|
41
|
+
with open(full_path) as file:
|
|
42
|
+
self.options["TARGET"] = list(filter(None, file.read().split('\n')))
|
|
43
|
+
else:
|
|
44
|
+
self.options["TARGET"] = list(
|
|
45
|
+
filter(None, self.options["TARGET"].split(",")))
|
|
46
|
+
# Clean up targets
|
|
47
|
+
for i in range(len(self.options["TARGET"])):
|
|
48
|
+
url = self.options["TARGET"][i]
|
|
49
|
+
# Inject protocol if not there
|
|
50
|
+
if not re.match(r'http(s?):', url):
|
|
51
|
+
url = 'http://' + url
|
|
52
|
+
|
|
53
|
+
parsed = urlsplit(url)
|
|
54
|
+
host = parsed.netloc
|
|
55
|
+
|
|
56
|
+
self.options["TARGET"][i] = host
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
ip_str = socket.gethostbyname(host)
|
|
60
|
+
ColorPrint.green(f"Searching for subdomains for {ip_str} ({host})")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.handle_exception(e,
|
|
63
|
+
"Error connecting to target! Make sure you spelled it correctly and it is a resolvable address")
|
|
64
|
+
|
|
65
|
+
def run(self):
|
|
66
|
+
# Retrieve IP of target and run initial configurations
|
|
67
|
+
self.init()
|
|
68
|
+
# If multiple targets, create scans for each
|
|
69
|
+
for i in range(len(self.options["TARGET"])):
|
|
70
|
+
# Default scans that run every time
|
|
71
|
+
target = self.options["TARGET"][i]
|
|
72
|
+
ColorPrint.green(f"Working on target: {target}")
|
|
73
|
+
threads = [threading.Thread(target=dns_zonetransfer, args=(self, target)),
|
|
74
|
+
threading.Thread(target=subdomain_hackertarget,
|
|
75
|
+
args=(self, target)),
|
|
76
|
+
threading.Thread(target=search_subject_alt_name,
|
|
77
|
+
args=(self, target)),
|
|
78
|
+
threading.Thread(target=search_netcraft, args=(self, target)),
|
|
79
|
+
threading.Thread(target=search_hudsonrock, args=(self, target)),
|
|
80
|
+
threading.Thread(target=search_crtsh, args=(self, target)),
|
|
81
|
+
threading.Thread(target=search_spyse,
|
|
82
|
+
args=(self, target)),
|
|
83
|
+
threading.Thread(target=search_anubisdb, args=(self, target))]
|
|
84
|
+
|
|
85
|
+
# Additional options - shodan.io scan
|
|
86
|
+
if self.options["--additional-info"]:
|
|
87
|
+
threads.append(threading.Thread(target=search_shodan, args=(self,)))
|
|
88
|
+
|
|
89
|
+
# Additional options - nmap scan of dnssec script and a host/port scan
|
|
90
|
+
if self.options["--with-nmap"]:
|
|
91
|
+
threads.append(
|
|
92
|
+
threading.Thread(target=dnssecc_subdomain_enum, args=(self, target)))
|
|
93
|
+
threads.append(threading.Thread(target=scan_host, args=(self, target)))
|
|
94
|
+
|
|
95
|
+
# Start all threads and wait for them to finish
|
|
96
|
+
for x in threads:
|
|
97
|
+
x.start()
|
|
98
|
+
|
|
99
|
+
for x in threads:
|
|
100
|
+
x.join()
|
|
101
|
+
|
|
102
|
+
# Run a recursive search on each subdomain - rarely useful, but nice to have
|
|
103
|
+
# just in case
|
|
104
|
+
if self.options["--recursive"]:
|
|
105
|
+
recursive_search(self)
|
|
106
|
+
|
|
107
|
+
# remove duplicates and clean up
|
|
108
|
+
self.domains = self.clean_domains(self.domains)
|
|
109
|
+
self.dedupe = set(self.domains)
|
|
110
|
+
|
|
111
|
+
print("Found", len(self.dedupe), "subdomains")
|
|
112
|
+
print("----------------")
|
|
113
|
+
|
|
114
|
+
if self.options["--ip"]:
|
|
115
|
+
self.resolve_ips()
|
|
116
|
+
else:
|
|
117
|
+
for domain in self.dedupe:
|
|
118
|
+
cleaned_domain = domain.strip()
|
|
119
|
+
ColorPrint.green(cleaned_domain)
|
|
120
|
+
if self.options['--silent']:
|
|
121
|
+
sys.stdout.write(cleaned_domain + '\n', override=True)
|
|
122
|
+
|
|
123
|
+
if self.options["--send-to-anubis-db"]:
|
|
124
|
+
send_to_anubisdb(self, [target])
|
|
125
|
+
# reset per domain
|
|
126
|
+
self.domains = list()
|
|
127
|
+
|
|
128
|
+
def resolve_ips(self):
|
|
129
|
+
unique_ips = set()
|
|
130
|
+
for domain in self.dedupe:
|
|
131
|
+
try:
|
|
132
|
+
# Attempt to get IP
|
|
133
|
+
resolved_ip = socket.gethostbyname(domain)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
# If getting IP fails, fallback to empty string
|
|
136
|
+
resolved_ip = ""
|
|
137
|
+
# TODO - Align domains and ips in stdout
|
|
138
|
+
ColorPrint.green(domain + ": " + resolved_ip)
|
|
139
|
+
if self.options['--silent']:
|
|
140
|
+
sys.stdout.write(domain + '\n', override=True)
|
|
141
|
+
|
|
142
|
+
if resolved_ip:
|
|
143
|
+
unique_ips.add(resolved_ip)
|
|
144
|
+
print("Found %s unique IPs" % len(unique_ips))
|
|
145
|
+
for ip in unique_ips:
|
|
146
|
+
# Ignore empty strings, final sanity check
|
|
147
|
+
if ip:
|
|
148
|
+
ColorPrint.green(ip)
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def clean_domains(domains):
|
|
152
|
+
cleaned = []
|
|
153
|
+
for subdomain in domains:
|
|
154
|
+
subdomain = subdomain.lower()
|
|
155
|
+
if subdomain.find("//") != -1:
|
|
156
|
+
subdomain = subdomain[subdomain.find("//") + 2:]
|
|
157
|
+
# Some pkey return instances like example.com. - remove the final .
|
|
158
|
+
if subdomain.endswith('.'):
|
|
159
|
+
subdomain = subdomain[:-1]
|
|
160
|
+
# sometimes we'll get something like /www.example.com
|
|
161
|
+
if subdomain[0] in ["\\", ".", "/", "#", "$", "%"]:
|
|
162
|
+
subdomain = subdomain[1:]
|
|
163
|
+
# If it's an email address, only take the domain part
|
|
164
|
+
if "@" in subdomain:
|
|
165
|
+
subdomain = subdomain.split("@")
|
|
166
|
+
# If it's an actual email like mail@example.com, take example.com
|
|
167
|
+
if len(subdomain) > 1:
|
|
168
|
+
subdomain = subdomain[1]
|
|
169
|
+
else:
|
|
170
|
+
# If for some reason it's example.com@, take example.com
|
|
171
|
+
subdomain = subdomain[0]
|
|
172
|
+
|
|
173
|
+
cleaned.append(subdomain.strip())
|
|
174
|
+
return cleaned
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from json import loads
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from anubis.utils.color_print import ColorPrint
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def search_anubisdb(self, target):
|
|
9
|
+
print("Searching Anubis-DB")
|
|
10
|
+
res = requests.get("https://anubisdb.com/subdomains/" + target)
|
|
11
|
+
if res.status_code == 200 and res.text:
|
|
12
|
+
subdomains = loads(res.text)
|
|
13
|
+
for subdomain in subdomains:
|
|
14
|
+
if subdomain not in self.domains:
|
|
15
|
+
self.domains.append(subdomain)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def send_to_anubisdb(self, target):
|
|
19
|
+
if len(target) == 1:
|
|
20
|
+
print("Sending to AnubisDB")
|
|
21
|
+
data = {'subdomains': self.domains}
|
|
22
|
+
# Sends found subdomains to Anubis (max 10,000/post)
|
|
23
|
+
res = requests.post("https://anubisdb.com/subdomains/" + target[0],
|
|
24
|
+
json=data)
|
|
25
|
+
if res.status_code != 200:
|
|
26
|
+
ColorPrint.red("Error sending results to AnubisDB - Status Code: " + str(
|
|
27
|
+
res.status_code))
|
|
28
|
+
else:
|
|
29
|
+
print("Cannot send multiple domains to AnubisDB")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import censys
|
|
2
|
+
|
|
3
|
+
from anubis.utils.color_print import ColorPrint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def search_censys(self, target):
|
|
7
|
+
print("Searching Censys")
|
|
8
|
+
try:
|
|
9
|
+
from anubis.API import CENSYS_ID, CENSYS_SECRET
|
|
10
|
+
except ImportError:
|
|
11
|
+
ColorPrint.red(
|
|
12
|
+
"To run a Censys scan, you must add your API keys to anubis/API.py")
|
|
13
|
+
return
|
|
14
|
+
if not CENSYS_SECRET or not CENSYS_ID:
|
|
15
|
+
ColorPrint.red(
|
|
16
|
+
"To run a Censys scan, you must add your API keys to anubis/API.py")
|
|
17
|
+
return
|
|
18
|
+
# Print certificate information for domains
|
|
19
|
+
c = censys.certificates.CensysCertificates(CENSYS_ID, CENSYS_SECRET)
|
|
20
|
+
for cert in c.search("." + target):
|
|
21
|
+
print(cert)
|
anubis/scanners/crt.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from html.parser import HTMLParser
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def search_crtsh(self, target):
|
|
8
|
+
"""
|
|
9
|
+
Search crt.sh for certificates related to the target and extract both
|
|
10
|
+
subdomains and root domains.
|
|
11
|
+
"""
|
|
12
|
+
print("Searching crt.sh")
|
|
13
|
+
headers = {
|
|
14
|
+
'authority': 'crt.sh',
|
|
15
|
+
'cache-control': 'max-age=0',
|
|
16
|
+
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.28 Safari/537.36',
|
|
17
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
|
|
18
|
+
'accept-encoding': 'gzip, deflate, br',
|
|
19
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Clean target input to ensure we have just the domain
|
|
23
|
+
target = clean_domain(target)
|
|
24
|
+
|
|
25
|
+
# Search for both wildcard and explicit domain
|
|
26
|
+
params = (('q', '%.' + target),)
|
|
27
|
+
try:
|
|
28
|
+
res = requests.get('https://crt.sh/', headers=headers, params=params)
|
|
29
|
+
scraped = res.text
|
|
30
|
+
|
|
31
|
+
# Extract domains from response
|
|
32
|
+
domains = extract_domains(scraped, target)
|
|
33
|
+
|
|
34
|
+
# Add all found domains to our list
|
|
35
|
+
for domain in domains:
|
|
36
|
+
if domain not in self.domains:
|
|
37
|
+
self.domains.append(domain)
|
|
38
|
+
if self.options["--verbose"]:
|
|
39
|
+
print("Crt.sh Found Domain:", domain)
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
self.handle_exception(e, "Error searching crt.sh")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clean_domain(domain):
|
|
46
|
+
"""Clean input to get the base domain without protocol or path."""
|
|
47
|
+
# Remove protocol if present
|
|
48
|
+
domain = re.sub(r'^(http|https)://', '', domain)
|
|
49
|
+
# Remove path, query params, etc.
|
|
50
|
+
domain = domain.split('/')[0].strip()
|
|
51
|
+
return domain
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _CertificateNamesParser(HTMLParser):
|
|
55
|
+
"""Collect direct cell text, separating names at BR and cell boundaries.
|
|
56
|
+
|
|
57
|
+
Unsupported nested markup discards the current name and the rest of its cell.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
_CELL_BOUNDARIES = ('td', 'th', 'tr', 'thead', 'tbody', 'tfoot', 'table')
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
super().__init__(convert_charrefs=True)
|
|
64
|
+
self.names = []
|
|
65
|
+
self._parts = None
|
|
66
|
+
|
|
67
|
+
def _finish_name(self):
|
|
68
|
+
if self._parts is not None:
|
|
69
|
+
self.names.append(''.join(self._parts))
|
|
70
|
+
self._parts = None
|
|
71
|
+
|
|
72
|
+
def handle_starttag(self, tag, attrs):
|
|
73
|
+
if tag == 'td':
|
|
74
|
+
self._finish_name()
|
|
75
|
+
self._parts = []
|
|
76
|
+
elif tag == 'br' and self._parts is not None:
|
|
77
|
+
self._finish_name()
|
|
78
|
+
self._parts = []
|
|
79
|
+
elif tag in self._CELL_BOUNDARIES:
|
|
80
|
+
self._finish_name()
|
|
81
|
+
elif self._parts is not None:
|
|
82
|
+
# Do not interpret scripts, links, or other nested markup as names.
|
|
83
|
+
self._parts = None
|
|
84
|
+
|
|
85
|
+
def handle_endtag(self, tag):
|
|
86
|
+
if tag in self._CELL_BOUNDARIES:
|
|
87
|
+
self._finish_name()
|
|
88
|
+
elif tag == 'br' and self._parts is not None:
|
|
89
|
+
self._finish_name()
|
|
90
|
+
self._parts = []
|
|
91
|
+
elif self._parts is not None:
|
|
92
|
+
self._parts = None
|
|
93
|
+
|
|
94
|
+
def handle_data(self, data):
|
|
95
|
+
if self._parts is not None:
|
|
96
|
+
self._parts.append(data)
|
|
97
|
+
|
|
98
|
+
def close(self):
|
|
99
|
+
super().close()
|
|
100
|
+
self._finish_name()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def extract_domains(html_content, target):
|
|
104
|
+
"""Return concrete in-scope hostnames, always including the normalized target.
|
|
105
|
+
|
|
106
|
+
Wildcard certificate names are skipped: stripping ``*.`` would invent a
|
|
107
|
+
concrete hostname. As before, labels preceding the caller's target must be
|
|
108
|
+
ASCII DNS labels. The target suffix is not revalidated or converted via IDNA.
|
|
109
|
+
"""
|
|
110
|
+
target = target.strip().lower()
|
|
111
|
+
domains = {target}
|
|
112
|
+
parser = _CertificateNamesParser()
|
|
113
|
+
parser.feed(html_content)
|
|
114
|
+
parser.close()
|
|
115
|
+
label_pattern = re.compile(r'[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?')
|
|
116
|
+
target_labels = len(target.split('.'))
|
|
117
|
+
|
|
118
|
+
for name in parser.names:
|
|
119
|
+
domain = name.strip()
|
|
120
|
+
if '*' in domain or len(domain.rstrip('.')) > 253:
|
|
121
|
+
continue
|
|
122
|
+
if target.isascii() and not domain.isascii():
|
|
123
|
+
continue
|
|
124
|
+
prefix_labels = domain.split('.')[:-target_labels]
|
|
125
|
+
domain = domain.lower()
|
|
126
|
+
if domain != target and not domain.endswith('.' + target):
|
|
127
|
+
continue
|
|
128
|
+
if all(label.isascii() and label_pattern.fullmatch(label.lower())
|
|
129
|
+
for label in prefix_labels):
|
|
130
|
+
domains.add(domain)
|
|
131
|
+
|
|
132
|
+
return list(domains)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from nmap import nmap
|
|
4
|
+
|
|
5
|
+
from anubis.utils.color_print import ColorPrint
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def dnssecc_subdomain_enum(self, target):
|
|
9
|
+
# Must run as root
|
|
10
|
+
if os.getuid() == 0:
|
|
11
|
+
print("Starting DNSSEC Enum")
|
|
12
|
+
nm = nmap.PortScanner()
|
|
13
|
+
arguments = '-sSU -p 53 --script dns-nsec-enum --script-args dns-nsec-enum.domains=' + target
|
|
14
|
+
|
|
15
|
+
nm.scan(hosts=target, arguments=arguments)
|
|
16
|
+
for host in nm.all_hosts():
|
|
17
|
+
try:
|
|
18
|
+
print(nm[host]['udp'][53]['script']['dns-nsec-enum'])
|
|
19
|
+
except:
|
|
20
|
+
pass
|
|
21
|
+
else:
|
|
22
|
+
ColorPrint.red(
|
|
23
|
+
"To run a DNSSEC subdomain enumeration, Anubis must be run as root")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def subdomain_hackertarget(self, target):
|
|
5
|
+
print("Searching HackerTarget")
|
|
6
|
+
headers = {
|
|
7
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36', }
|
|
8
|
+
params = (('q', target),)
|
|
9
|
+
|
|
10
|
+
results = requests.get('http://api.hackertarget.com/hostsearch/',
|
|
11
|
+
headers=headers, params=params)
|
|
12
|
+
results = results.text.split('\n')
|
|
13
|
+
for res in results:
|
|
14
|
+
try:
|
|
15
|
+
if res.split(",")[0] != "":
|
|
16
|
+
domain = res.split(",")[0]
|
|
17
|
+
domain = domain.strip()
|
|
18
|
+
if domain not in self.domains and domain.endswith("." + target):
|
|
19
|
+
self.domains.append(domain)
|
|
20
|
+
if self.options["--verbose"]:
|
|
21
|
+
print("HackerTarget Found Domain:", domain.strip())
|
|
22
|
+
except:
|
|
23
|
+
print("except")
|
|
24
|
+
pass
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from urllib.parse import urlsplit
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def search_hudsonrock(self, target):
|
|
7
|
+
print("Searching HudsonRock")
|
|
8
|
+
try:
|
|
9
|
+
res = requests.get("https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-domain?domain=" + target)
|
|
10
|
+
urls = res.json().get('data', {}).get('all_urls', [])
|
|
11
|
+
except Exception as e:
|
|
12
|
+
self.handle_exception(e, "Error searching HudsonRock")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
for url_entry in urls:
|
|
16
|
+
try:
|
|
17
|
+
# Entries are full URLs (https://sso.example.com/login); keep the host
|
|
18
|
+
domain = urlsplit(url_entry.get('url', '')).hostname
|
|
19
|
+
except (AttributeError, ValueError):
|
|
20
|
+
continue
|
|
21
|
+
if domain and domain.endswith("." + target) and domain not in self.domains:
|
|
22
|
+
self.domains.append(domain)
|
|
23
|
+
if self.options["--verbose"]:
|
|
24
|
+
print("HudsonRock Found Domain:", domain)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def search_netcraft(self, target):
|
|
7
|
+
print("Searching NetCraft.com")
|
|
8
|
+
headers = {'Pragma': 'no-cache', 'DNT': '1',
|
|
9
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
10
|
+
'Accept-Language': 'en-US,en;q=0.9,it;q=0.8',
|
|
11
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
|
|
12
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
|
|
13
|
+
'Cache-Control': 'no-cache',
|
|
14
|
+
'Referer': 'https://searchdns.netcraft.com/?restriction=site+ends+with&host=',
|
|
15
|
+
'Connection': 'keep-alive', }
|
|
16
|
+
|
|
17
|
+
params = (('restriction', 'site contains'), ('host', target))
|
|
18
|
+
try:
|
|
19
|
+
res = requests.get('https://searchdns.netcraft.com/', headers=headers,
|
|
20
|
+
params=params)
|
|
21
|
+
scraped = res.text
|
|
22
|
+
trimmed = scraped[scraped.find('<div class="blogtitle">'):scraped.rfind(
|
|
23
|
+
'<div id="copyright">')]
|
|
24
|
+
subdomain_finder = re.compile(
|
|
25
|
+
r'<a href="http://toolbar.netcraft.com/site_report\?url=(.*)">')
|
|
26
|
+
links = subdomain_finder.findall(trimmed)
|
|
27
|
+
for domain in links:
|
|
28
|
+
if domain.strip() not in self.domains and domain.endswith("." + target):
|
|
29
|
+
self.domains.append(domain.strip())
|
|
30
|
+
if self.options["--verbose"]:
|
|
31
|
+
print("Netcraft Found Domain:", domain.strip())
|
|
32
|
+
except Exception as e:
|
|
33
|
+
self.handle_exception(e, "Error searching NetCraft")
|
|
34
|
+
pass
|
anubis/scanners/nmap.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from json import dumps
|
|
3
|
+
|
|
4
|
+
import nmap
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Performs an nmap scan of a target, and outputs interesting services/ssl information
|
|
8
|
+
def scan_host(self, target):
|
|
9
|
+
if shutil.which("nmap") is None:
|
|
10
|
+
print("Nmap must be installed to run --with-nmap!")
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
print("Starting Nmap Scan")
|
|
14
|
+
nm = nmap.PortScanner()
|
|
15
|
+
arguments = self.options["--overwrite-nmap-scan"] or '-nPn -sV -sC'
|
|
16
|
+
nm.scan(hosts=target, arguments=arguments)
|
|
17
|
+
# Could be extended to scan multiple hosts in the future
|
|
18
|
+
for host in nm.all_hosts():
|
|
19
|
+
if self.options["--verbose"]:
|
|
20
|
+
print(dumps(nm[host], indent=2, sort_keys=True))
|
|
21
|
+
print('Host : %s (%s)' % (host, nm[host].hostname()))
|
|
22
|
+
for proto in nm[host].all_protocols():
|
|
23
|
+
print('----------')
|
|
24
|
+
print('Protocol: %s' % proto)
|
|
25
|
+
lport = nm[host][proto].keys()
|
|
26
|
+
for port in lport:
|
|
27
|
+
print('port: %s\tstate: %s' % (port, nm[host][proto][port]['state']))
|
|
28
|
+
try:
|
|
29
|
+
if nm[host][proto][port]['product']:
|
|
30
|
+
print('\tservice: %s' % nm[host][proto][port]['product'], end='')
|
|
31
|
+
if nm[host][proto][port]['version']:
|
|
32
|
+
print('\tversion: %s' % nm[host][proto][port]['version'])
|
|
33
|
+
else:
|
|
34
|
+
print('')
|
|
35
|
+
except Exception as e:
|
|
36
|
+
self.handle_exception(e)
|
|
37
|
+
try:
|
|
38
|
+
fix_newline = nm[host][proto][port]['script']['ssl-cert'].split('\n')
|
|
39
|
+
print('\tCertificate:')
|
|
40
|
+
for i in range(len(fix_newline)):
|
|
41
|
+
print('\t', fix_newline[i])
|
|
42
|
+
if fix_newline[i].startswith("Subject Alternative Name: "):
|
|
43
|
+
content = fix_newline[i].replace("Subject Alternative Name: ", '')
|
|
44
|
+
content = content.replace("DNS:", '')
|
|
45
|
+
new_domains = content.split(",")
|
|
46
|
+
for domain in new_domains:
|
|
47
|
+
domain = domain.strip()
|
|
48
|
+
if domain not in self.domains:
|
|
49
|
+
self.domains.append(domain)
|
|
50
|
+
if self.options["--verbose"]:
|
|
51
|
+
print("Nmap Found Domain:", domain.strip())
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.handle_exception(e)
|