├── anubis ├── scanners │ ├── __init__.py │ ├── dnssec.py │ ├── censys.py │ ├── shodan.py │ ├── hudson_rock.py │ ├── hackertarget.py │ ├── anubis_db.py │ ├── spyse.py │ ├── zonetransfer.py │ ├── ssl.py │ ├── recursive.py │ ├── netcraft.py │ ├── pkey.py │ ├── dnsdumpster.py │ ├── nmap.py │ └── crt.py ├── utils │ ├── __init__.py │ ├── color_print.py │ ├── signal_handler.py │ └── search_worker.py ├── __init__.py ├── commands │ ├── __init__.py │ ├── base.py │ └── target.py ├── API.py └── cli.py ├── .dockerignore ├── tests ├── domains.txt ├── test_cli.py └── commands │ └── test_target.py ├── renovate.json ├── setup.cfg ├── requirements.txt ├── .travis.yml ├── MANIFEST.in ├── Dockerfile ├── ISSUE_TEMPLATE.md ├── coverage.svg ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── setup.py ├── CODE_OF_CONDUCT.md └── README.md /anubis/scanners/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /anubis/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | tests/ 3 | -------------------------------------------------------------------------------- /anubis/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.3.1' 2 | -------------------------------------------------------------------------------- /anubis/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .target import * 2 | -------------------------------------------------------------------------------- /tests/domains.txt: -------------------------------------------------------------------------------- 1 | example.com 2 | https://example.com 3 | https://jonlu.ca -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | [metadata] 4 | description_file = README.md -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | python_nmap==0.7.1 3 | shodan>=1.31.0 4 | docopt==0.6.2 5 | requests>=2.32.3 6 | censys==2.2.16 7 | dnspython>=2.7.0 -------------------------------------------------------------------------------- /anubis/API.py: -------------------------------------------------------------------------------- 1 | # Public Shodan key bundled with Anubis 2 | CENSYS_ID = "" 3 | CENSYS_SECRET = "" 4 | # Spyse Token - https://spyse.com/user 5 | SPYSE_TOKEN = "" 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | 5 | # command to install dependencies 6 | install: 7 | - pip3 install -r requirements.txt 8 | - pip3 install . 9 | # command to run tests 10 | script: 11 | - pytest # or py.test for Python versions 3.5 and below -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude .gitignore 2 | exclude .coverage 3 | exclude .travis.yml 4 | include README.rst 5 | include setup.cfg 6 | include requirements.txt 7 | prune .cache 8 | prune .git 9 | prune build 10 | prune dist 11 | recursive-exclude *.egg-info * 12 | recursive-include tests * 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-slim 2 | 3 | RUN apt-get update && apt-get install -y --no-install-recommends \ 4 | build-essential \ 5 | libssl-dev \ 6 | libffi-dev \ 7 | python-dev \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | WORKDIR /Anubis/ 11 | COPY . /Anubis/ 12 | 13 | RUN pip3 install . 14 | 15 | ENTRYPOINT ["anubis"] 16 | -------------------------------------------------------------------------------- /anubis/commands/base.py: -------------------------------------------------------------------------------- 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') 15 | -------------------------------------------------------------------------------- /anubis/scanners/dnssec.py: -------------------------------------------------------------------------------- 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") 24 | -------------------------------------------------------------------------------- /anubis/scanners/censys.py: -------------------------------------------------------------------------------- 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) 22 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Please follow the general troubleshooting steps first: 2 | 3 | - [ ] Check you are using Python >3.11 4 | - [ ] Make sure you are using pip3 (or any instance of pip associated with Python 3.x) 5 | 6 | ### Bug reports: 7 | 8 | Make sure you include the following: 9 | 10 | * Anubis Version 11 | * Python Version 12 | * Full stack trace if crash or installation error 13 | * Machine details (OS, build number, distro, etc.) 14 | 15 | Also, please assign @jonluca to all issues at first. 16 | 17 | Please replace this line with a brief summary of your issue. 18 | 19 | ### Features: 20 | 21 | **Please note by far the quickest way to get a new feature is to make a Pull Request.** 22 | 23 | Otherwise, open an issue and tag it with "feature-request" 24 | -------------------------------------------------------------------------------- /anubis/utils/color_print.py: -------------------------------------------------------------------------------- 1 | class ColorPrint: 2 | RED = '\033[91m' 3 | GREEN = '\033[92m' 4 | YELLOW = '\033[93m' 5 | LIGHT_PURPLE = '\033[94m' 6 | PURPLE = '\033[95m' 7 | END = '\033[0m' 8 | 9 | @classmethod 10 | def red(self, s, **kwargs): 11 | print(self.RED + s + self.END, **kwargs) 12 | 13 | @classmethod 14 | def green(self, s, **kwargs): 15 | print(self.GREEN + s + self.END, **kwargs) 16 | 17 | @classmethod 18 | def yellow(self, s, **kwargs): 19 | print(self.YELLOW + s + self.END, **kwargs) 20 | 21 | @classmethod 22 | def light_purple(self, s, **kwargs): 23 | print(self.LIGHT_PURPLE + s + self.END, **kwargs) 24 | 25 | @classmethod 26 | def purple(self, s, **kwargs): 27 | print(self.PURPLE + s + self.END, **kwargs) 28 | -------------------------------------------------------------------------------- /anubis/utils/signal_handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | class SignalHandler: 5 | """ 6 | The object that will handle signals and stop the worker threads. 7 | """ 8 | 9 | #: The stop event that's shared by this handler and threads. 10 | stopper = None 11 | 12 | #: The pool of worker threads 13 | workers = None 14 | 15 | def __init__(self, stopper, workers): 16 | self.stopper = stopper 17 | self.workers = workers 18 | 19 | def __call__(self, signum, frame): 20 | """ 21 | This will be called by the python signal module 22 | 23 | https://docs.python.org/3/library/signal.html#signal.signal 24 | """ 25 | self.stopper.set() 26 | 27 | for worker in self.workers: 28 | worker.join() 29 | 30 | sys.__stdout__.write("Quitting...") 31 | sys.__stdout__.flush() 32 | sys.exit(0) 33 | -------------------------------------------------------------------------------- /anubis/scanners/shodan.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import shodan 4 | 5 | def search_shodan(self): 6 | print("Searching Shodan.io for additional information") 7 | api_key = os.environ.get("SHODAN_API_KEY", None) 8 | if api_key is None: 9 | return 10 | 11 | api = shodan.Shodan(api_key) 12 | for i in range(len(self.options["TARGET"])): 13 | try: 14 | results = api.host(socket.gethostbyname(self.options["TARGET"][i])) 15 | 16 | print('Server Location: ' + str(results['city']) + ", " + str( 17 | results['country_code']) + ' - ' + str(results['postal_code'])) 18 | 19 | print("ISP or Hosting Company: %s" % str(results['isp'])) 20 | 21 | if results['os'] is not None: 22 | print("Possible OS: %s" % str(results['os'])) 23 | except Exception as e: 24 | self.handle_exception(e, "Error retrieving additional info") 25 | -------------------------------------------------------------------------------- /anubis/scanners/hudson_rock.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | 3 | import requests 4 | 5 | from anubis.utils.color_print import ColorPrint 6 | 7 | 8 | def search_hudsonrock(self, target): 9 | try: 10 | print("Searching HudsonRock") 11 | res = requests.get("https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-domain?domain=" + target) 12 | data = res.json() 13 | if hasattr(data, "data") and hasattr(data['data'], 'all_urls'): 14 | urls = data['data']['all_urls'] 15 | for url_entry in urls: 16 | if hasattr(url_entry, 'url'): 17 | url = url_entry['url'] 18 | if url not in self.domains: 19 | self.domains.append(url) 20 | if self.options["--verbose"]: 21 | print("HudsonRock Found Domain:", url) 22 | 23 | except: 24 | print("Error searching HudsonRock") 25 | return -------------------------------------------------------------------------------- /anubis/scanners/hackertarget.py: -------------------------------------------------------------------------------- 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 25 | -------------------------------------------------------------------------------- /coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 74% 19 | 74% 20 | 21 | 22 | -------------------------------------------------------------------------------- /anubis/scanners/anubis_db.py: -------------------------------------------------------------------------------- 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") 30 | -------------------------------------------------------------------------------- /anubis/scanners/spyse.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | 3 | import requests 4 | 5 | from anubis.API import SPYSE_TOKEN 6 | 7 | 8 | def search_spyse(self, target): 9 | if SPYSE_TOKEN: 10 | print("Searching Spyse") 11 | headers = { 12 | 'accept': 'application/json', 13 | 'Authorization': f"Bearer {SPYSE_TOKEN}", 14 | 'Content-Type': 'application/json', 15 | } 16 | 17 | data = {"limit": 100, "offset": 0, "search_params": [], "query": target} 18 | 19 | domains = [] 20 | try: 21 | response = requests.post('https://api.spyse.com/v4/data/domain/search', headers=headers, json=data) 22 | list_results = loads(response.text) 23 | if 'data' in list_results: 24 | for item in list_results['data']['items']: 25 | domains.append(item['name']) 26 | if domains: 27 | self.domains.extend(domains) 28 | except Exception as e: 29 | print("Exception when searching spyse") 30 | return 31 | if domains and self.options["--verbose"]: 32 | for res in domains: 33 | print("Spyse Found Domain:", res) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 JonLuca De Caro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /anubis/scanners/zonetransfer.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | import dns.query 4 | import dns.resolver 5 | import dns.zone 6 | 7 | 8 | # Checks for zone transfers on that domain. Very rare to succeed, but when you 9 | # do, it's a gold mine 10 | def dns_zonetransfer(self, target): 11 | print("Testing for zone transfers") 12 | 13 | zonetransfers = [] 14 | resolver = dns.resolver.Resolver() 15 | 16 | try: 17 | answers = resolver.query(target, 'NS') 18 | except Exception as e: 19 | self.handle_exception(e, "Error checking for Zone Transfers") 20 | return 21 | 22 | resolved_ips = [] 23 | 24 | for ns in answers: 25 | ns = str(ns).rstrip('.') 26 | resolved_ips.append(socket.gethostbyname(ns)) 27 | 28 | for ip in resolved_ips: 29 | try: 30 | zone = dns.zone.from_xfr(dns.query.xfr(ip, target)) 31 | for name, node in zone.nodes.items(): 32 | name = str(name) 33 | if name not in ["@", "*"]: 34 | zonetransfers.append(name + '.' + target) 35 | except: 36 | pass 37 | 38 | if zonetransfers: 39 | print("\tZone transfers possible:") 40 | for zone in zonetransfers: 41 | print(zone) 42 | -------------------------------------------------------------------------------- /anubis/scanners/ssl.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import socket 3 | import ssl 4 | from socket import gaierror 5 | 6 | 7 | def search_subject_alt_name(self, target): 8 | print("Searching for Subject Alt Names") 9 | try: 10 | context = ssl.create_default_context() 11 | 12 | # Do connectivity testing to ensure SSLyze is able to connect 13 | try: 14 | with socket.create_connection((target, 443)) as sock: 15 | with context.wrap_socket(sock, server_hostname=target) as ssock: 16 | # https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.getpeercert 17 | cert = ssock.getpeercert() 18 | 19 | subjectAltName = defaultdict(set) 20 | for type_, san in cert['subjectAltName']: 21 | subjectAltName[type_].add(san) 22 | 23 | dns_domains = list(subjectAltName['DNS']) 24 | for domain in dns_domains: 25 | if domain: 26 | self.domains.append(domain.strip()) 27 | except gaierror as e: 28 | # Could not connect to the server; abort 29 | print(f"Error connecting to {target}: {e}") 30 | return 31 | 32 | except Exception as e: 33 | self.handle_exception(e) 34 | -------------------------------------------------------------------------------- /anubis/scanners/recursive.py: -------------------------------------------------------------------------------- 1 | """The target command.""" 2 | import queue 3 | import signal 4 | import sys 5 | import threading 6 | from io import StringIO 7 | 8 | from anubis.utils.search_worker import SearchWorker 9 | from anubis.utils.signal_handler import SignalHandler 10 | 11 | 12 | def recursive_search(self): 13 | print("Starting recursive search - warning, might take a long time") 14 | domains = self.clean_domains(self.domains) 15 | domains_unique = set(domains) 16 | num_workers = 10 17 | 18 | if self.options["--queue-workers"]: 19 | num_workers = int(self.options["--queue-workers"]) 20 | 21 | stopper = threading.Event() 22 | url_queue = queue.Queue() 23 | for domain in domains_unique: 24 | url_queue.put(domain) 25 | 26 | # we need to keep track of the workers but not start them yet 27 | workers = [SearchWorker(url_queue, self.domains, stopper, self) for _ in 28 | range(num_workers)] 29 | 30 | # create our signal handler and connect it 31 | handler = SignalHandler(stopper, workers) 32 | signal.signal(signal.SIGINT, handler) 33 | 34 | if not self.options["--verbose"]: 35 | # catch stdout and replace it with our own 36 | self.stdout, sys.stdout = sys.stdout, StringIO() 37 | 38 | # start the threads! 39 | for worker in workers: 40 | worker.start() 41 | 42 | # wait for the queue to empty 43 | url_queue.join() 44 | 45 | sys.stdout = self.stdout 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | .pytest_cache/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .idea/* 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Environments 49 | .env 50 | .venv 51 | env/ 52 | venv/ 53 | ENV/ 54 | env.bak/ 55 | venv.bak/ 56 | 57 | .idea_modules/ 58 | 59 | docs/reference.md 60 | demos/*/parts/ 61 | demos/*/prime/ 62 | demos/*/stage/ 63 | demos/*/snap/.snapcraft/ 64 | demos/**/*.snap 65 | snap/.snapcraft/ 66 | tests/unit/parts/ 67 | tests/unit/snap/ 68 | tests/unit/stage/ 69 | build 70 | dist 71 | *.egg-info 72 | .eggs/ 73 | *.pyc 74 | .coverage** 75 | htmlcov 76 | __pycache__ 77 | docs/**.html 78 | Cargo.lock 79 | target 80 | *.swp 81 | *.snap 82 | parts 83 | stage 84 | prime 85 | .DS_Store -------------------------------------------------------------------------------- /anubis/scanners/netcraft.py: -------------------------------------------------------------------------------- 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('
'):scraped.rfind( 23 | '