├── .gitignore ├── data ├── gist_keywords.txt ├── interesting_files_verify.csv ├── google_dorks.txt ├── av_domains.lst ├── github_dorks.txt ├── template_html.html └── template_media.html ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── module_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── README.md └── modules ├── recon ├── hosts-locations │ └── migrate_hosts.py ├── ports-hosts │ ├── migrate_ports.py │ └── ssl_scan.py ├── domains-hosts │ ├── threatminer.py │ ├── threatcrowd.py │ ├── ssl_san.py │ ├── hackertarget.py │ ├── certificate_transparency.py │ ├── binaryedge.py │ ├── spyse_subdomains.py │ ├── shodan_hostname.py │ ├── bing_domain_api.py │ ├── google_site_web.py │ ├── netcraft.py │ ├── censys_domain.py │ ├── bing_domain_web.py │ ├── builtwith.py │ └── brute_hosts.py ├── hosts-ports │ ├── binaryedge.py │ └── shodan_ip.py ├── contacts-domains │ ├── migrate_contacts.py │ └── censys_email_to_domains.py ├── companies-domains │ ├── whoxy_dns.py │ ├── pen.py │ ├── viewdns_reverse_whois.py │ └── censys_subdomains.py ├── hosts-domains │ └── migrate_hosts.py ├── hosts-hosts │ ├── virustotal.py │ ├── ipstack.py │ ├── resolve.py │ ├── reverse_resolve.py │ ├── censys_ip.py │ ├── bing_ip.py │ ├── censys_hostname.py │ ├── ipinfodb.py │ └── censys_query.py ├── domains-companies │ ├── whoxy_whois.py │ ├── pen.py │ └── censys_companies.py ├── netblocks-hosts │ ├── virustotal.py │ ├── reverse_resolve.py │ ├── shodan_net.py │ └── censys_netblock.py ├── netblocks-companies │ ├── whois_orgs.py │ └── censys_netblock_company.py ├── profiles-contacts │ ├── github_users.py │ └── bing_linkedin_contacts.py ├── netblocks-ports │ ├── census_2012.py │ └── censysio.py ├── locations-locations │ ├── geocode.py │ └── reverse_geocode.py ├── profiles-profiles │ ├── twitter_mentioned.py │ ├── twitter_mentions.py │ ├── namechk.py │ └── profiler.py ├── contacts-contacts │ ├── mailtester.py │ ├── mangle.py │ └── abc.py ├── repositories-vulnerabilities │ ├── github_dorks.py │ └── gists_search.py ├── domains-contacts │ ├── pen.py │ ├── pgp_search.py │ ├── whois_pocs.py │ ├── hunter_io.py │ └── wikileaker.py ├── contacts-credentials │ ├── hibp_breach.py │ └── hibp_paste.py ├── companies-contacts │ ├── pen.py │ └── censys_email_address.py ├── companies-multi │ ├── github_miner.py │ ├── shodan_org.py │ ├── censys_tls_subjects.py │ └── censys_org.py ├── domains-vulnerabilities │ ├── xssed.py │ └── ghdb.py ├── locations-pushpins │ ├── twitter.py │ ├── youtube.py │ ├── shodan.py │ └── flickr.py ├── credentials-credentials │ ├── hashes_org.py │ └── bozocrack.py ├── repositories-profiles │ └── github_commits.py ├── profiles-repositories │ └── github_repos.py ├── domains-domains │ └── brute_suffix.py └── contacts-profiles │ └── fullcontact.py ├── reporting ├── proxifier.py ├── xlsx.py ├── list.py ├── json.py ├── xml.py └── csv.py ├── import ├── list.py ├── masscan.py └── nmap.py ├── discovery └── info_disclosure │ └── cache_snoop.py └── exploitation └── injection └── command_injector.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /data/gist_keywords.txt: -------------------------------------------------------------------------------- 1 | password 2 | Password 3 | PASSWORD 4 | -------------------------------------------------------------------------------- /data/interesting_files_verify.csv: -------------------------------------------------------------------------------- 1 | robots.txt,user-agent: 2 | sitemap.xml,Apache Status< 9 | jmx-console/,JBoss 10 | admin-console/,index.seam 11 | web-console/,Administration 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | title: '[Short version of your question here]' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **To save some time, have you:** 11 | - [ ] Checked the [wiki](https://github.com/lanmaster53/recon-ng/wiki) 12 | - [ ] Checked previously answered [questions](https://github.com/lanmaster53/recon-ng-marketplace/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aclosed+label%3Aquestion) 13 | 14 | **Please ask the question below** 15 | \[Please be as detailed as possible.\] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome to the Recon-ng Marketplace! The official module repository for the Recon-ng Framework. 2 | 3 | For guidance on contributing to or developing modules, see the [Development Guide](https://github.com/lanmaster53/recon-ng/wiki/Development-Guide) in the official [Recon-ng wiki](https://github.com/lanmaster53/recon-ng/wiki). 4 | 5 | **This repository is not intended for independent use.** The Recon-ng Marketplace is used from within the Recon-ng Framework. To download and use Recon-ng, visit the [official Recon-ng Framework repository](https://github.com/lanmaster53/recon-ng). 6 | -------------------------------------------------------------------------------- /modules/recon/hosts-locations/migrate_hosts.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import os 3 | import re 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'Hosts to Locations Data Migrator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Adds a new location for all the locations stored in the \'hosts\' table.', 12 | 'query': 'SELECT DISTINCT latitude, longitude FROM hosts WHERE latitude IS NOT NULL AND longitude IS NOT NULL', 13 | } 14 | 15 | def module_run(self, locations): 16 | for location in locations: 17 | self.insert_locations(latitude=location[0], longitude=location[1]) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/module_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Module Request 3 | about: Suggest a new module for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is the feature request related to a tool? Please describe.** 11 | \[A clear and concise description of the tool.\] 12 | 13 | **Does the tool have a web API?** 14 | \[If so, does the API require a key? Provide a link to the documentation.\] 15 | 16 | **Describe the expected module functionality.** 17 | \[A clear and concise description of what the module should do.\] 18 | - Expected input: 19 | - Expected output: 20 | 21 | **Additional context** 22 | \[Any other context or screenshots about the feature request.\] 23 | -------------------------------------------------------------------------------- /data/google_dorks.txt: -------------------------------------------------------------------------------- 1 | # backups 2 | ext:backup | ext:bak | ext:bkf | ext:bkp | ext:old 3 | 4 | # config files 5 | ext:cfg | ext:cnf | ext:conf | ext:inf | ext:ini | ext:ora | ext:rdp | ext:reg | ext:txt | ext:xml 6 | 7 | # db files 8 | ext:dbf | ext:mdb | ext:sql 9 | 10 | # directory indexing 11 | intitle:index.of 12 | 13 | # docs 14 | ext:csv | ext:doc | ext:docx | ext:odt | ext:pdf | ext:pps | ext:ppt | ext:pptx | ext:psw | ext:rtf | ext:sxw | ext:xls | ext:xlsx 15 | 16 | # logs 17 | ext:log 18 | 19 | # sql errors 20 | intext:"incorrect syntax near" | intext:"sql syntax near" | intext:"syntax error has occurred" | intext:"unexpected end of SQL command" | intext:"Warning: mysql_connect()" | intext:"Warning: mysql_query()" | intext:"Warning: pg_connect()" 21 | -------------------------------------------------------------------------------- /modules/recon/ports-hosts/migrate_ports.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import re 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Ports to Hosts Data Migrator', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.0', 10 | 'description': 'Adds a new host for all the hostnames stored in the \'ports\' table.', 11 | } 12 | 13 | def module_run(self): 14 | # ip address regex 15 | regex = r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' 16 | # get a list of hosts that are not ip addresses 17 | hosts = [x[0] for x in self.query('SELECT DISTINCT host FROM ports WHERE host IS NOT NULL') if not re.match(regex, x[0])] 18 | for host in hosts: 19 | self.insert_hosts(host=host) 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Module Name** 11 | Which module is affected? 12 | https://github.com/lanmaster53/recon-ng-marketplace/tree/master/modules/[FILL ME IN] 13 | 14 | **Bug Description** 15 | \[A clear and concise description of the bug.\] 16 | 17 | **Steps to Reproduce** 18 | \[Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error\] 23 | 24 | **Expected Behavior** 25 | \[A clear and concise description of the expected behavior.\] 26 | 27 | **Screenshots** 28 | \[If applicable, screenshots to help explain the problem.\] 29 | 30 | **Additional Context** 31 | \[Any other context about the problem.\] 32 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/threatminer.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'ThreatMiner DNS lookup', 7 | 'author': 'Pedro Rodrigues', 8 | 'version': '1.0', 9 | 'description': 'Use ThreatMiner API to discover subdomains.', 10 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 11 | } 12 | 13 | def module_run(self, domains): 14 | for domain in domains: 15 | self.heading(domain, level=0) 16 | resp = self.request('GET', f"https://api.threatminer.org/v2/domain.php?rt=5&q={domain}") 17 | if resp.json().get('status_code') == '200': 18 | for subdomain in resp.json().get('results'): 19 | self.insert_hosts(host=subdomain) 20 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/threatcrowd.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'ThreatCrowd DNS lookup', 7 | 'author': 'mike2dot0', 8 | 'version': '1.0', 9 | 'description': 'Leverages the ThreatCrowd passive DNS API to discover hosts/subdomains.', 10 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 11 | } 12 | 13 | def module_run(self, domains): 14 | for domain in domains: 15 | self.heading(domain, level=0) 16 | resp = self.request('GET', f"https://www.threatcrowd.org/searchApi/v2/domain/report/?domain={domain}") 17 | if resp.json().get('response_code') == '1': 18 | for subdomain in resp.json().get('subdomains'): 19 | self.insert_hosts(host=subdomain) 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request, make sure to complete the following:** 2 | - [ ] Ensure there are no similar pull requests. 3 | - [ ] Read the [Development Guide](https://github.com/lanmaster53/recon-ng/wiki/Development-Guide). 4 | 5 | **What kind of PR is this?** 6 | _Please add an 'x' in the appropriate box, and apply a label to the PR matching the type here._ 7 | - [ ] Bug Fix 8 | - [ ] New Module 9 | - [ ] Documentation Update 10 | 11 | **Checklist For Approval** 12 | - [ ] Updated the meta dictionary for the module. 13 | - If bug fix, updated the version. 14 | - [ ] Indexed the module 15 | - [ ] Added the index to the `modules.yml` file 16 | - [ ] Made the most out of the available [mixins](https://github.com/lanmaster53/recon-ng/wiki/Development-Guide#mixins). 17 | - [ ] Ensured the code is PEP8 compliant with `pycodestyle` or `black`. 18 | -------------------------------------------------------------------------------- /modules/reporting/proxifier.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.threads import ThreadingMixin 3 | 4 | class Module(BaseModule, ThreadingMixin): 5 | 6 | meta = { 7 | 'name': 'Proxifier', 8 | 'author': 'AverageSecurityGuy (@averagesecguy)', 9 | 'version': '1.0', 10 | 'description': 'Requests URLs from the database for the purpose of populating an inline proxy. Requires that the global proxy option be set prior to running the module.', 11 | 'query': 'SELECT example FROM vulnerabilities WHERE category=\'Google Dork\'', 12 | } 13 | 14 | def module_run(self, urls): 15 | self.thread(urls) 16 | 17 | def module_thread(self, url): 18 | try: 19 | resp = self.request('GET', url) 20 | self.verbose(f"{url} => {resp.status_code}") 21 | except Exception as e: 22 | self.error(f"{url} => {e}") 23 | -------------------------------------------------------------------------------- /data/av_domains.lst: -------------------------------------------------------------------------------- 1 | dnl-01.geo.kaspersky.com 2 | download797.avast.com 3 | downloads2.kaspersky-labs.com 4 | es-latest-3.sophos.com/update 5 | es-web-2.sophos.com 6 | es-web-2.sophos.com.edgesuite.net 7 | es-web.sophos.com 8 | es-web.sophos.com.edgesuite.net 9 | forefrontdl.microsoft.com 10 | guru.avg.com 11 | liveupdate.symantec.com 12 | liveupdate.symantecliveupdate.com 13 | osce8-p.activeupdate.trendmicro.com 14 | update.nai.com 15 | update.symantec.com 16 | www.dnl-01.geo.kaspersky.com 17 | www.download797.avast.com 18 | www.downloads2.kaspersky-labs.com 19 | www.es-latest-3.sophos.com/update 20 | www.es-web-2.sophos.com 21 | www.es-web-2.sophos.com.edgesuite.net 22 | www.es-web.sophos.com 23 | www.es-web.sophos.com.edgesuite.net 24 | www.forefrontdl.microsoft.com 25 | www.guru.avg.com 26 | www.liveupdate.symantec.com 27 | www.liveupdate.symantecliveupdate.com 28 | www.osce8-p.activeupdate.trendmicro.com 29 | www.update.nai.com 30 | www.update.symantec.com 31 | -------------------------------------------------------------------------------- /data/github_dorks.txt: -------------------------------------------------------------------------------- 1 | # http://blog.conviso.com.br/2013/06/github-hacking-for-fun-and-sensitive.html 2 | # private keys 3 | extension:pem private 4 | extension:conf FTP server configuration 5 | # email addresses 6 | extension:xls mail 7 | extension:sql mysql dump 8 | # possible PHP backdoor 9 | stars:>1000 forks:>100 extension:php "eval(preg_replace(" 10 | 11 | # http://seclists.org/fulldisclosure/2014/Mar/343 12 | # database passwords 13 | mysql.binero.se 14 | define("DB_PASSWORD" 15 | 16 | # http://seclists.org/fulldisclosure/2013/Jun/15 17 | # possible SQL injection 18 | extension:php mysql_query $_GET 19 | 20 | # https://twitter.com/egyp7/status/628955613528109056 21 | # rails secret token 22 | filename:secret_token.rb config 23 | language:ruby secret_token 24 | 25 | # https://twitter.com/lanmaster53/status/629102944252772356 26 | # Flask apps with possible SSTI vulns 27 | extension:py flask render_template_string 28 | 29 | # https://twitter.com/TekDefense/status/294556153151647744 30 | # md5 hash of most used password 123456 31 | e10adc3949ba59abbe56e057f20f883e 32 | 33 | # private keys 34 | path:.ssh/id_rsa BEGIN 35 | -------------------------------------------------------------------------------- /modules/recon/hosts-ports/binaryedge.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'BinaryEdge.io Ports lookup', 7 | 'author': 'Ryan Hays', 8 | 'version': '1.0', 9 | 'description': 'Uses the BinaryEdge API to discover open services for IP Addresses.', 10 | 'required_keys': ['binaryedge_api'], 11 | 'query': 'SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT NULL', 12 | } 13 | 14 | def module_run(self, ipaddrs): 15 | key = self.keys.get('binaryedge_api') 16 | for ipaddr in ipaddrs: 17 | self.heading(ipaddr, level=0) 18 | resp = self.request('GET', f"https://api.binaryedge.io/v2/query/ip/{ipaddr}", headers={'X-Key': key}) 19 | if resp.status_code == 200: 20 | for event in resp.json().get('events'): 21 | for result in event['results']: 22 | self.insert_ports(ip_address=ipaddr, port=result['target']['port'], 23 | protocol=result['target']['protocol']) 24 | -------------------------------------------------------------------------------- /modules/import/list.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'List File Importer', 7 | 'author': 'Tim Tomes (@lanmaster53)', 8 | 'version': '1.1', 9 | 'description': 'Imports values from a list file into a database table and column.', 10 | 'options': ( 11 | ('filename', None, True, 'path and filename for list input'), 12 | ('table', None, True, 'table to import the list values'), 13 | ('column', None, True, 'column to import the list values'), 14 | ), 15 | } 16 | 17 | def module_run(self): 18 | count = 0 19 | with open(self.options['filename']) as fh: 20 | lines = fh.read().split() 21 | method = 'insert_'+self.options['table'].lower() 22 | if not hasattr(self, method): 23 | self.error(f"No such table: {self.options['table']}") 24 | return 25 | func = getattr(self, method) 26 | for line in lines: 27 | self.output(line) 28 | kwargs = {self.options['column']: line} 29 | count += func(**kwargs) 30 | self.output(f"{count} new records added.") 31 | -------------------------------------------------------------------------------- /modules/recon/contacts-domains/migrate_contacts.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import os 3 | import re 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'Contacts to Domains Data Migrator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.1', 11 | 'description': 'Adds a new domain for all the hostnames associated with email addresses stored in the \'contacts\' table.', 12 | 'comments': ( 13 | 'This modules considers that everything after the first element could contain other hosts besides the current. Therefore, hosts > 2 domains deep will create domains > 2 elements in length.', 14 | ), 15 | 'query': 'SELECT DISTINCT email FROM contacts WHERE email IS NOT NULL', 16 | 'files': ['suffixes.txt'], 17 | } 18 | 19 | def module_run(self, emails): 20 | # extract the host portion of each email address 21 | hosts = [x.split('@')[1] for x in emails] 22 | with open(os.path.join(self.data_path, 'suffixes.txt')) as f: 23 | suffixes = [line.strip().lower() for line in f if len(line)>0 and line[0] != '#'] 24 | domains = self.hosts_to_domains(hosts, suffixes) 25 | for domain in domains: 26 | self.insert_domains(domain=domain) 27 | -------------------------------------------------------------------------------- /modules/import/masscan.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from xml.etree import ElementTree 3 | 4 | 5 | class Module(BaseModule): 6 | meta = { 7 | 'name': 'Masscan XML Output Importer', 8 | 'author': 'Ryan Hays (@_ryanhays)', 9 | 'version': '1.0', 10 | 'description': 'Imports hosts and ports into the respective databases from Masscan XML output.', 11 | 'options': ( 12 | ('filename', None, True, 'path and filename for list input'), 13 | ), 14 | } 15 | 16 | def module_run(self): 17 | with open(self.options['filename'], 'rt') as f: 18 | tree = ElementTree.parse(f) 19 | 20 | for host in tree.findall('host'): 21 | address = host.find('address') 22 | if address.attrib.get('addrtype') == 'ipv4': 23 | ipaddress = address.attrib.get('addr') 24 | self.insert_hosts(ip_address=ipaddress) 25 | 26 | for host_port in host.find('ports').findall('port'): 27 | if host_port.find('state').get('state') != 'open': 28 | continue 29 | 30 | self.insert_ports(ip_address=ipaddress, port=host_port.attrib.get('portid'), 31 | protocol=host_port.attrib.get('protocol')) 32 | -------------------------------------------------------------------------------- /modules/recon/companies-domains/whoxy_dns.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'Whoxy Company DNS Lookup', 7 | 'author': 'Ryan Hays (@_ryanhays)', 8 | 'version': '1.1', 9 | 'description': 'Uses the Whoxy API to query DNS records belonging to a company', 10 | 'required_keys': ['whoxy_api'], 11 | 'query': 'SELECT DISTINCT company FROM companies WHERE company IS NOT NULL', 12 | } 13 | 14 | def module_run(self, companies): 15 | key = self.keys.get('whoxy_api') 16 | for company in companies: 17 | self.heading(company, level=0) 18 | cur_page = 1 19 | total_pages = 1 20 | 21 | while cur_page <= total_pages: 22 | resp = self.request('GET', f"http://api.whoxy.com/?key={key}&reverse=whois&company={company}&page={cur_page}") 23 | if resp.json().get('total_results') <= 0: 24 | break 25 | 26 | cur_page = resp.json().get('current_page') 27 | total_pages = resp.json().get('total_pages') 28 | 29 | for domain in resp.json().get('search_result'): 30 | self.insert_hosts(host=domain['domain_name']) 31 | 32 | cur_page += 1 33 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/ssl_san.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import json 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'SSL SAN Lookup', 8 | 'author': 'Zach Grace (@ztgrace) zgrace@403labs.com and Bryan Onel (@BryanOnel86) onel@oneleet.com', 9 | 'version': '1.0', 10 | 'description': 'Uses the ssltools.com API to obtain the Subject Alternative Names for a domain. Updates the \'hosts\' table with the results.', 11 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 12 | } 13 | 14 | def module_run(self, domains): 15 | for domain in domains: 16 | self.heading(domain, level=0) 17 | url = 'http://www.ssltools.com/api/scan' 18 | resp = self.request('POST', url, data={'url': domain}) 19 | if not resp.json()['response']: 20 | self.output(f"SSL endpoint not reachable or response invalid for '{domain}'") 21 | continue 22 | if not resp.json()['response']['san_entries']: 23 | self.output(f"No Subject Alternative Names found for '{domain}'") 24 | continue 25 | hosts = [x.strip() for x in resp.json()['response']['san_entries'] if '*' not in x] 26 | for host in hosts: 27 | self.insert_hosts(host) 28 | -------------------------------------------------------------------------------- /modules/recon/hosts-domains/migrate_hosts.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import os 3 | import re 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'Hosts to Domains Data Migrator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.1', 11 | 'description': 'Adds a new domain for all the hostnames stored in the \'hosts\' table.', 12 | 'comments': ( 13 | 'This modules considers that everything after the first element could contain other hosts besides the current. Therefore, hosts > 2 domains deep will create domains > 2 elements in length.', 14 | ), 15 | 'query': 'SELECT DISTINCT host FROM hosts WHERE host IS NOT NULL', 16 | 'files': ['suffixes.txt'], 17 | } 18 | 19 | def module_run(self, hosts): 20 | # ip address regex 21 | regex = r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' 22 | # only migrate hosts that aren't ip addresses 23 | hosts = [x for x in hosts if not re.match(regex, x[0])] 24 | with open(os.path.join(self.data_path, 'suffixes.txt')) as f: 25 | suffixes = [line.strip().lower() for line in f if len(line)>0 and line[0] != '#'] 26 | domains = self.hosts_to_domains(hosts, suffixes) 27 | for domain in domains: 28 | self.insert_domains(domain=domain) 29 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/virustotal.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from time import sleep 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Virustotal domains extractor', 8 | 'author': 'USSC (thanks @jevalenciap)', 9 | 'version': '1.0', 10 | 'description': 'Harvests domains from the Virustotal by using the report API. Updates the \'hosts\' table with the results.', 11 | 'required_keys': ['virustotal_api'], 12 | 'query': 'SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT NULL', 13 | 'options': ( 14 | ('interval', 15, True, 'interval in seconds between api requests'), 15 | ), 16 | } 17 | 18 | def module_run(self, addresses): 19 | key = self.keys.get('virustotal_api') 20 | url = 'https://www.virustotal.com/vtapi/v2/ip-address/report' 21 | for ip in addresses: 22 | self.heading(ip, level=0) 23 | resp = self.request('GET', url, params={'ip': ip, 'apikey': key}) 24 | if resp.json() and 'resolutions' in resp.json().keys(): 25 | for entry in resp.json()['resolutions']: 26 | hostname = entry.get('hostname') 27 | if hostname: 28 | self.insert_hosts(host=hostname, ip_address=ip) 29 | sleep(self.options['interval']) 30 | -------------------------------------------------------------------------------- /modules/recon/domains-companies/whoxy_whois.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Whoxy Whois Lookup', 8 | 'author': 'Ryan Hays (@_ryanhays)', 9 | 'version': '1.1', 10 | 'description': 'Uses the Whoxy API to query whois information for a domain and updates the companies and ' 11 | 'contacts tables. ', 12 | 'required_keys': ['whoxy_api'], 13 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 14 | } 15 | 16 | def module_run(self, domains): 17 | key = self.keys.get('whoxy_api') 18 | for domain in domains: 19 | self.heading(domain, level=0) 20 | resp = self.request('GET', f"http://api.whoxy.com/?key={key}&whois={domain}") 21 | if resp.status_code == 200: 22 | reg = resp.json().get('registrant_contact') 23 | adm = resp.json().get('administrative_contact') 24 | tech = resp.json().get('technical_contact') 25 | 26 | for email in [reg['email_address'], adm['email_address'], tech['email_address']]: 27 | self.insert_contacts(email=email) 28 | 29 | for company in [reg['company_name'], adm['company_name'], tech['company_name']]: 30 | self.insert_companies(company=company) 31 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/hackertarget.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | meta = { 5 | 'name': 'HackerTarget Lookup', 6 | 'author': 'Michael Henriksen (@michenriksen)', 7 | 'version': '1.1', 8 | 'description': 'Uses the HackerTarget.com API to find host names. Updates the \'hosts\' table with the results.', 9 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 10 | } 11 | 12 | def module_run(self, domains): 13 | for domain in domains: 14 | self.heading(domain, level=0) 15 | url = 'https://api.hackertarget.com/hostsearch/' 16 | payload = {'q': domain} 17 | resp = self.request('GET', url, params=payload) 18 | if resp.status_code != 200: 19 | self.error(f"Got unexpected response code: {resp.status_code}") 20 | continue 21 | if resp.text == '': 22 | self.output('No results found.') 23 | continue 24 | if resp.text.startswith('error'): 25 | self.error(resp.text) 26 | continue 27 | for line in resp.text.split("\n"): 28 | line = line.strip() 29 | if line == '': 30 | continue 31 | host, address = line.split(",") 32 | self.insert_hosts(host=host, ip_address=address) 33 | -------------------------------------------------------------------------------- /modules/recon/netblocks-hosts/virustotal.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from time import sleep 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Virustotal domains extractor', 8 | 'author': 'USSC (thanks @jevalenciap)', 9 | 'version': '1.0', 10 | 'description': 'Harvests domains from the Virustotal by using the report API. Updates the \'hosts\' table with the results.', 11 | 'required_keys': ['virustotal_api'], 12 | 'query': 'SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT NULL', 13 | 'options': ( 14 | ('interval', 15, True, 'interval in seconds between api requests'), 15 | ), 16 | } 17 | 18 | def module_run(self, netblocks): 19 | key = self.keys.get('virustotal_api') 20 | url = 'https://www.virustotal.com/vtapi/v2/ip-address/report' 21 | for netblock in netblocks: 22 | for ip in self.cidr_to_list(netblock): 23 | self.heading(ip, level=0) 24 | resp = self.request('GET', url, params={'ip': ip, 'apikey': key}) 25 | if resp.json() and 'resolutions' in resp.json().keys(): 26 | for entry in resp.json()['resolutions']: 27 | hostname = entry.get('hostname') 28 | if hostname: 29 | self.insert_hosts(host=hostname, ip_address=ip) 30 | sleep(self.options['interval']) 31 | -------------------------------------------------------------------------------- /modules/reporting/xlsx.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import os 3 | import xlsxwriter 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'XLSX File Creator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Creates an Excel compatible XLSX file containing the entire data set.', 12 | 'options': ( 13 | ('filename', os.path.join(BaseModule.workspace, 'results.xlsx'), True, 'path and filename for output'), 14 | ), 15 | } 16 | 17 | def module_run(self): 18 | filename = self.options['filename'] 19 | # create a new xlsx file 20 | with xlsxwriter.Workbook(filename, {'strings_to_urls': False}) as workbook: 21 | tables = self.get_tables() 22 | # loop through all tables in the database 23 | for table in tables: 24 | # create a worksheet for the table 25 | worksheet = workbook.add_worksheet(table) 26 | # build the data set 27 | rows = [tuple([x[0] for x in self.get_columns(table)])] 28 | rows.extend(self.query(f'SELECT * FROM "{table}"')) 29 | # write the rows of data to the xlsx file 30 | for r in range(0, len(rows)): 31 | for c in range(0, len(rows[r])): 32 | worksheet.write(r, c, rows[r][c]) 33 | self.output(f"All data written to '{filename}'.") 34 | -------------------------------------------------------------------------------- /modules/recon/netblocks-companies/whois_orgs.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'Whois Company Harvester', 7 | 'author': 'Tim Tomes (@lanmaster53)', 8 | 'version': '1.0', 9 | 'description': 'Uses the ARIN Whois RWS to harvest Companies data from whois queries for the given netblock. Updates the \'companies\' table with the results.', 10 | 'query': 'SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT NULL', 11 | } 12 | 13 | def module_run(self, netblocks): 14 | headers = {'Accept': 'application/json'} 15 | for netblock in netblocks: 16 | self.heading(netblock, level=0) 17 | urls = [f"http://whois.arin.net/rest/cidr/{netblock}", f"http://whois.arin.net/rest/ip/{netblock.split('/')[0]}"] 18 | for url in urls: 19 | self.verbose(f"URL: {url}") 20 | resp = self.request('GET', url, headers=headers) 21 | if 'No record found for the handle provided.' in resp.text: 22 | self.output('No companies found.') 23 | continue 24 | for ref in ['orgRef', 'customerRef']: 25 | if ref in resp.json()['net']: 26 | company = resp.json()['net'][ref]['@name'] 27 | handle = resp.json()['net'][ref]['$'] 28 | self.insert_companies(company=company, description=handle) 29 | -------------------------------------------------------------------------------- /modules/recon/domains-companies/pen.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import re 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'IANA Private Enterprise Number Company-by-Domain Lookup', 8 | 'author': 'Jonathan M. Wilbur ', 9 | 'version': '1.1', 10 | 'description': 'Given a domain, finds companies in the IANA Private Enterprise Number (PEN) registry and adds them to the \'companies\' table.', 11 | 'required_keys': [], 12 | 'comments': (), 13 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 14 | 'options': (), 15 | } 16 | 17 | def module_run(self, domains): 18 | url = 'https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers' 19 | resp = self.request('GET', url) 20 | if resp.status_code != 200: 21 | self.alert('When retrieving IANA PEN Registry, got HTTP status code ' + str(resp.status_code) + '!') 22 | for domain in domains: 23 | dom = re.escape(domain) 24 | regex = r'(\d+)\s*\n\s{2}(.*)\s*\n\s{4}(.*)\s*\n\s{6}(.*)' + dom + r'\s*\n' 25 | matchfound = False 26 | for match in re.finditer(regex, resp.text, re.IGNORECASE): 27 | company = match.groups()[1] 28 | self.insert_companies(company) 29 | matchfound = True 30 | if not matchfound: 31 | self.alert('No matches found for domain \'' + domain + '\'') 32 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/certificate_transparency.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import json 3 | 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'Certificate Transparency Search', 9 | 'author': 'Rich Warren (richard.warren@nccgroup.trust)', 10 | 'version': '1.3', 11 | 'description': 'Searches certificate transparency data from crt.sh, adding newly identified hosts to the hosts ' 12 | 'table.', 13 | 'comments': ( 14 | 'A longer global TIMEOUT setting may be required for larger domains.', 15 | ), 16 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 17 | } 18 | 19 | def module_run(self, domains): 20 | for domain in domains: 21 | self.heading(domain, level=0) 22 | resp = self.request( 23 | 'GET', 24 | f"https://crt.sh/?q=%25.{domain}&output=json", 25 | headers={"Accept": "application/json"}, 26 | ) 27 | 28 | if resp.status_code != 200: 29 | self.output(f"Invalid response for '{domain}'") 30 | continue 31 | 32 | for cert in resp.json(): 33 | for host in cert.get('name_value').split(): 34 | if '@' in host: 35 | self.insert_contacts(email=host) 36 | self.insert_hosts(host.split('@')[1]) 37 | else: 38 | self.insert_hosts(host) 39 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/ipstack.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import json 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'ipstack', 8 | 'author': 'Siarhei Harbachou (Tech.Insiders), Gerrit Helm (G) and Tim Tomes (@lanmaster53)', 9 | 'version': '1.0', 10 | 'description': 'Leverages the ipstack.com API to geolocate a host by IP address. Updates the \'hosts\' table with the results.', 11 | 'required_keys': ['ipstack_api'], 12 | 'query': 'SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT NULL', 13 | } 14 | 15 | def module_run(self, hosts): 16 | for host in hosts: 17 | api_key = self.keys.get('ipstack_api') 18 | url = f"http://api.ipstack.com/{host}?access_key={api_key}" 19 | resp = self.request('GET', url) 20 | try: 21 | jsonobj = resp.json() 22 | except ValueError: 23 | self.error(f"Invalid JSON response for '{host}'.\n{resp.text}") 24 | continue 25 | region = ', '.join([jsonobj[x] for x in ['city', 'region_name'] if jsonobj[x]]) or None 26 | country = jsonobj['country_name'] 27 | latitude = jsonobj['latitude'] 28 | longitude = jsonobj['longitude'] 29 | self.output(f"{host} - {latitude},{longitude} - {', '.join([x for x in [region, country] if x])}") 30 | self.query('UPDATE hosts SET region=?, country=?, latitude=?, longitude=? WHERE ip_address=?', (region, country, latitude, longitude, host)) 31 | -------------------------------------------------------------------------------- /modules/recon/profiles-contacts/github_users.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.github import GithubMixin 3 | from recon.utils.parsers import parse_name 4 | from urllib.parse import quote_plus 5 | 6 | class Module(BaseModule, GithubMixin): 7 | meta = { 8 | 'name': 'Github Profile Harvester', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Uses the Github API to gather user info from harvested profiles. Updates the \'contacts\' table with the results.', 12 | 'required_keys': ['github_api'], 13 | 'query': "SELECT DISTINCT username FROM profiles WHERE username IS NOT NULL AND resource LIKE 'Github'", 14 | } 15 | 16 | def module_run(self, usernames): 17 | for username in usernames: 18 | users = self.query_github_api(endpoint=f"/users/{quote_plus(username)}") 19 | # should only be one result, but loop just in case 20 | for user in users: 21 | name = user['name'] 22 | fname, mname, lname = parse_name(name or '') 23 | email = user['email'] 24 | title = 'Github Contributor' 25 | if user['company']: 26 | title += f" at {user['company']}" 27 | region = user['location'] 28 | # don't add if lacking meaningful data 29 | if any((fname, lname, email)): 30 | self.insert_contacts(first_name=fname, middle_name=mname, last_name=lname, email=email, title=title, region=region) 31 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/binaryedge.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'BinaryEdge.io DNS lookup', 7 | 'author': 'Ryan Hays', 8 | 'version': '1.2', 9 | 'description': 'Uses the BinaryEdge API to discover subdomains.', 10 | 'required_keys': ['binaryedge_api'], 11 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 12 | } 13 | 14 | def module_run(self, domains): 15 | key = self.keys.get('binaryedge_api') 16 | for domain in domains: 17 | self.heading(domain, level=0) 18 | page_num = 1 19 | domain_count = 0 20 | total_ans = 1 21 | while domain_count < total_ans: 22 | resp = self.request('GET', f"https://api.binaryedge.io/v2/query/domains/dns/{domain}?page={page_num}", 23 | headers={'X-Key': key}) 24 | if resp.status_code == 200: 25 | total_ans = resp.json().get('total') 26 | 27 | for subdomain in resp.json().get('events'): 28 | domain_count += 1 29 | if "A" in subdomain: 30 | self.insert_hosts(host=subdomain['domain'], ip_address=subdomain['A'][0]) 31 | else: 32 | self.insert_hosts(host=subdomain['domain']) 33 | page_num += 1 34 | elif resp.json().get('status') == 400: 35 | break 36 | -------------------------------------------------------------------------------- /modules/recon/companies-domains/pen.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import re 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'IANA Private Enterprise Number Domain Getter', 8 | 'author': 'Jonathan M. Wilbur ', 9 | 'version': '1.1', 10 | 'description': 'Given a company name, gathers a domain from the email address of the registered IANA Private Enterprise Number (PEN) contact from the PEN registry and adds it to the \'domains\' table.', 11 | 'required_keys': [], 12 | 'comments': (), 13 | 'query': 'SELECT DISTINCT company FROM companies WHERE company IS NOT NULL', 14 | 'options': (), 15 | } 16 | 17 | def module_run(self, companies): 18 | url = 'https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers' 19 | resp = self.request('GET', url) 20 | if resp.status_code != 200: 21 | self.alert('When retrieving IANA PEN Registry, got HTTP status code ' + str(resp.status_code) + '!') 22 | for company in companies: 23 | comp = re.escape(company) 24 | regex = r'(\d+)\s*\n\s{2}.*' + comp + r'.*\s*\n\s{4}(.*)\s*\n\s{6}(.*)\s*\n' 25 | matchfound = False 26 | for match in re.finditer(regex, resp.text, re.IGNORECASE): 27 | domain = match.groups()[2].split('&')[1] 28 | self.insert_domains(domain) 29 | matchfound = True 30 | if not matchfound: 31 | self.alert('No matches found for company \'' + company + '\'') 32 | -------------------------------------------------------------------------------- /modules/recon/netblocks-ports/census_2012.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import re 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Internet Census 2012 Lookup', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.0', 10 | 'description': 'Queries the Internet Census 2012 data through Exfiltrated.com to enumerate open ports for a netblock.', 11 | 'comments': ( 12 | 'http://exfiltrated.com/querystart.php', 13 | ), 14 | 'query': 'SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT NULL', 15 | } 16 | 17 | def module_run(self, netblocks): 18 | url = 'http://exfiltrated.com/query.php' 19 | for netblock in netblocks: 20 | self.heading(netblock, level=0) 21 | addresses = self.cidr_to_list(netblock) 22 | first = addresses[0] 23 | last = addresses[-1] 24 | self.verbose(f"{netblock} ({first} - {last})") 25 | payload = {'startIP': first, 'endIP': last, 'includeHostnames': 'Yes', 'rawDownload': 'Yes'} 26 | resp = self.request('GET', url, params=payload) 27 | hosts = resp.text.strip().split('\r\n')[1:] 28 | for host in hosts: 29 | elements = host.split('\t') 30 | address = elements[1] 31 | port = elements[2] 32 | hostname = elements[0] 33 | self.insert_ports(ip_address=address, host=hostname, port=port) 34 | if not hosts: 35 | self.output('No scan data available.') 36 | -------------------------------------------------------------------------------- /modules/recon/locations-locations/geocode.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'Address Geocoder', 7 | 'author': 'Quentin Kaiser (contact@quentinkaiser.be)', 8 | 'version': '1.0', 9 | 'description': 'Queries the Google Maps API to obtain coordinates for an address. Updates the \'locations\' table with the results.', 10 | 'required_keys': ['google_api'], 11 | 'query': 'SELECT DISTINCT street_address FROM locations WHERE street_address IS NOT NULL', 12 | } 13 | 14 | def module_run(self, addresses): 15 | api_key = self.keys.get('google_api') 16 | for address in addresses: 17 | self.verbose(f"Geocoding '{address}'...") 18 | payload = {'address' : address, 'key' : api_key} 19 | url = 'https://maps.googleapis.com/maps/api/geocode/json' 20 | resp = self.request('GET', url, params=payload) 21 | # kill the module if nothing is returned 22 | if len(resp.json()['results']) == 0: 23 | self.output(f"Unable to geocode '{address}'.") 24 | return 25 | # loop through the results 26 | for result in resp.json()['results']: 27 | lat = result['geometry']['location']['lat'] 28 | lon = result['geometry']['location']['lng'] 29 | # store the result 30 | self.insert_locations(lat, lon, address) 31 | self.query('DELETE FROM locations WHERE street_address=? AND latitude IS NULL AND longitude IS NULL', (address,)) 32 | -------------------------------------------------------------------------------- /modules/recon/companies-domains/viewdns_reverse_whois.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from lxml.html import fromstring 3 | 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'Viewdns Reverse Whois Domain Harvester', 9 | 'author': 'Gaetan Ferry (@_mabote_) from @synacktiv', 10 | 'version': '1.1', 11 | 'description': 'Harvests domain names belonging to a company by using ' 12 | + 'the viewdns.info free reverse whois tool.', 13 | 'comments': ( 14 | 'Does not support company names < 6 characters', 15 | ), 16 | 'query': 'SELECT DISTINCT company FROM companies WHERE company IS NOT NULL', 17 | } 18 | 19 | def module_run(self, companies): 20 | url = 'https://viewdns.info/reversewhois/' 21 | for company in companies: 22 | self.heading(company, level=0) 23 | if len(company) < 6: 24 | self.alert('Company name too short, skipping') 25 | continue 26 | payload = {'q': company} 27 | resp = self.request('GET', url, params=payload) 28 | if resp.status_code != 200: 29 | self.alert('An error occured: ' + str(resp.status_code)) 30 | continue 31 | content = fromstring(resp.text) 32 | domains = content.xpath("//table[@border='1']//tr/td[1]//text()") 33 | if len(domains) <= 0: 34 | continue 35 | # remove table headers 36 | domains = domains[1::] 37 | for domain in domains: 38 | self.insert_domains(domain) 39 | -------------------------------------------------------------------------------- /modules/recon/profiles-profiles/twitter_mentioned.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.twitter import TwitterMixin 3 | import re 4 | 5 | class Module(BaseModule, TwitterMixin): 6 | 7 | meta = { 8 | 'name': 'Twitter Mentioned', 9 | 'author': 'Robert Frost (@frosty_1313, frosty[at]unluckyfrosty.net)', 10 | 'version': '1.0', 11 | 'description': 'Leverages the Twitter API to enumerate users that mentioned the given handle. Updates the \'profiles\' table with the results.', 12 | 'required_keys': ['twitter_api', 'twitter_secret'], 13 | 'comments': ( 14 | 'Twitter limits searchable tweet history to 7 days.', 15 | ), 16 | 'query': "SELECT DISTINCT username FROM profiles WHERE username IS NOT NULL AND resource LIKE 'Twitter' COLLATE NOCASE", 17 | 'options': ( 18 | ('limit', True, True, 'toggle rate limiting'), 19 | ), 20 | } 21 | 22 | def module_run(self, handles): 23 | for handle in handles: 24 | handle = handle if not handle.startswith('@') else handle[1:] 25 | self.heading(handle, level=0) 26 | for operand in ['to:', '@']: 27 | results = self.search_twitter_api({'q': f"{operand}{handle}"}, self.options['limit']) 28 | for tweet in results: 29 | handle = tweet['user']['screen_name'] 30 | name = tweet['user']['name'] 31 | time = tweet['created_at'] 32 | self.insert_profiles(username=handle, resource='Twitter', url='https://twitter.com/' + handle, category='social', notes=name) 33 | -------------------------------------------------------------------------------- /modules/recon/contacts-contacts/mailtester.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from lxml.html import fromstring 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'MailTester Email Validator', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.0', 10 | 'description': 'Leverages MailTester.com to validate email addresses.', 11 | 'query': 'SELECT DISTINCT email FROM contacts WHERE email IS NOT NULL', 12 | 'options': ( 13 | ('remove', False, True, 'remove invalid email addresses'), 14 | ), 15 | } 16 | 17 | def module_run(self, emails): 18 | url = 'http://www.mailtester.com/testmail.php' 19 | error = 'Too many requests from the same IP address.' 20 | payload = {'lang':'en'} 21 | for email in emails: 22 | payload['email'] = email 23 | resp = self.request('POST', url, data=payload) 24 | if error in resp.text: 25 | self.error(error) 26 | break 27 | tree = fromstring(resp.text) 28 | # clean up problematic HTML for debian based distros 29 | tree.forms[0].getparent().remove(tree.forms[0]) 30 | msg_list = tree.xpath('//table[last()]/tr[last()]/td[last()]/text()') 31 | msg = ' '.join([x.strip() for x in msg_list]) 32 | output = self.alert if 'is valid' in msg else self.verbose 33 | output(f"{email} => {msg}") 34 | if 'does not exist' in msg: 35 | self.query('UPDATE contacts SET email=NULL where email=?', (email,)) 36 | self.verbose(f"{email} removed.") 37 | -------------------------------------------------------------------------------- /modules/discovery/info_disclosure/cache_snoop.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import os 3 | import dns.message 4 | import dns.query 5 | import re 6 | 7 | 8 | class Module(BaseModule): 9 | 10 | meta = { 11 | 'name': 'DNS Cache Snooper', 12 | 'author': 'thrapt (thrapt@gmail.com)', 13 | 'version': '1.1', 14 | 'description': 'Uses the DNS cache snooping technique to check for visited domains', 15 | 'comments': ( 16 | 'Nameserver must be in IP form.', 17 | 'http://304geeks.blogspot.com/2013/01/dns-scraping-for-corporate-av-detection.html', 18 | ), 19 | 'options': ( 20 | ('nameserver', None, True, 'IP address of authoritative nameserver'), 21 | ('domains', os.path.join(BaseModule.data_path, 'av_domains.lst'), True, 'file containing the list of domains to snoop for'), 22 | ), 23 | 'files': ['av_domains.lst'], 24 | } 25 | 26 | def module_run(self): 27 | nameserver = self.options['nameserver'] 28 | with open(self.options['domains']) as fp: 29 | domains = [x.strip() for x in fp.read().split()] 30 | for domain in domains: 31 | response = None 32 | # prepare our query 33 | query = dns.message.make_query(domain, dns.rdatatype.A, dns.rdataclass.IN) 34 | # unset the Recurse flag 35 | query.flags ^= dns.flags.RD 36 | response = dns.query.udp(query, nameserver) 37 | if len(response.answer) > 0: 38 | self.alert(f"{domain} => Snooped!") 39 | else: 40 | self.verbose(f"{domain} => Not Found.") 41 | -------------------------------------------------------------------------------- /modules/reporting/list.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import codecs 3 | import os 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'List Creator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Creates a file containing a list of records from the database.', 12 | 'options': ( 13 | ('table', 'hosts', True, 'source table of data for the list'), 14 | ('column', 'ip_address', True, 'source column of data for the list'), 15 | ('unique', True, True, 'only return unique items from the dataset'), 16 | ('nulls', False, True, 'include NULLs in the dataset'), 17 | ('filename', os.path.join(BaseModule.workspace, 'list.txt'), True, 'path and filename for output'), 18 | ), 19 | } 20 | 21 | def module_run(self): 22 | filename = self.options['filename'] 23 | with codecs.open(filename, 'wb', encoding='utf-8') as outfile: 24 | # handle the source of information for the report 25 | column = self.options['column'] 26 | table = self.options['table'] 27 | nulls = f' WHERE "{column}" IS NOT NULL' if not self.options['nulls'] else '' 28 | unique = 'DISTINCT ' if self.options['unique'] else '' 29 | query = f'SELECT {unique}"{column}" FROM "{table}"{nulls} ORDER BY 1' 30 | rows = self.query(query) 31 | for row in [x[0] for x in rows]: 32 | row = row if row else '' 33 | outfile.write(f"{row}\n") 34 | print(row) 35 | self.output(f"{len(rows)} items added to '{filename}'.") 36 | -------------------------------------------------------------------------------- /modules/reporting/json.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import codecs 3 | import json 4 | import os 5 | 6 | class Module(BaseModule): 7 | 8 | meta = { 9 | 'name': 'JSON Report Generator', 10 | 'author': 'Paul (@PaulWebSec)', 11 | 'version': '1.0', 12 | 'description': 'Creates a JSON report.', 13 | 'options': ( 14 | ('tables', 'hosts, contacts, credentials', True, 'comma delineated list of tables'), 15 | ('filename', os.path.join(BaseModule.workspace, 'results.json'), True, 'path and filename for report output'), 16 | ), 17 | } 18 | 19 | def module_run(self): 20 | filename = self.options['filename'] 21 | with codecs.open(filename, 'wb', encoding='utf-8') as outfile: 22 | # build a list of table names 23 | tables = [x.strip() for x in self.options['tables'].split(',')] 24 | data_dict = {} 25 | cnt = 0 26 | for table in tables: 27 | data_dict[table] = [] 28 | columns = [x[0] for x in self.get_columns(table)] 29 | columns_str = '", "'.join(columns) 30 | rows = self.query(f'SELECT "{columns_str}" FROM "{table}" ORDER BY 1') 31 | for row in rows: 32 | row_dict = {} 33 | for i in range(0, len(columns)): 34 | row_dict[columns[i]] = row[i] 35 | data_dict[table].append(row_dict) 36 | cnt += 1 37 | # write the JSON to a file 38 | outfile.write(json.dumps(data_dict, indent=4)) 39 | self.output(f"{cnt} records added to '{filename}'.") 40 | -------------------------------------------------------------------------------- /modules/import/nmap.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from xml.etree import ElementTree 3 | 4 | 5 | class Module(BaseModule): 6 | meta = { 7 | 'name': 'Nmap XML Output Importer', 8 | 'author': 'Ryan Hays (@_ryanhays)', 9 | 'version': '1.1', 10 | 'description': 'Imports hosts and ports into the respective databases from Nmap XML output.', 11 | 'options': ( 12 | ('filename', None, True, 'path and filename for list input'), 13 | ), 14 | } 15 | 16 | def module_run(self): 17 | with open(self.options['filename'], 'rt') as f: 18 | tree = ElementTree.parse(f) 19 | 20 | for host in tree.findall('host'): 21 | for ip in host.findall('address'): 22 | ipaddress = ip.attrib.get('addr') 23 | try: 24 | for hostname in host.find('hostnames').findall('hostname'): 25 | self.insert_domains(domain=hostname.attrib.get('name')) 26 | self.insert_hosts(host=hostname.attrib.get('name'), ip_address=ipaddress) 27 | except AttributeError: 28 | self.insert_hosts(ip_address=ipaddress) 29 | 30 | try: 31 | for host_port in host.find('ports').findall('port'): 32 | if host_port.find('state').get('state') != 'open': 33 | continue 34 | port = host_port.attrib.get('portid') 35 | protocol = host_port.attrib.get('protocol') 36 | self.insert_ports(ip_address=ipaddress, port=port, protocol=protocol) 37 | except AttributeError: 38 | pass 39 | -------------------------------------------------------------------------------- /modules/recon/hosts-ports/shodan_ip.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import shodan 3 | import time 4 | 5 | 6 | class Module(BaseModule): 7 | 8 | meta = { 9 | 'name': 'Shodan IP Enumerator', 10 | 'author': 'Tim Tomes (@lanmaster53) and Matt Puckett (@t3lc0) & Ryan Hays (@_ryanhays)', 11 | 'version': '1.2', 12 | 'description': 'Harvests port information from the Shodan API by using the \'ip\' search operator. Updates the ' 13 | '\'ports\' table with the results.', 14 | 'required_keys': ['shodan_api'], 15 | 'query': 'SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT NULL', 16 | 'options': ( 17 | ('limit', 1, True, 'limit number of api requests per input source (0 = unlimited)'), 18 | ), 19 | 'dependencies': ['shodan'] 20 | } 21 | 22 | def module_run(self, ipaddrs): 23 | limit = self.options['limit'] 24 | api = shodan.Shodan(self.keys.get('shodan_api')) 25 | for ipaddr in ipaddrs: 26 | self.heading(ipaddr, level=0) 27 | try: 28 | ipinfo = api.host(ipaddr) 29 | for port in ipinfo['data']: 30 | try: 31 | for hostname in port['hostnames']: 32 | self.insert_ports(host=hostname, ip_address=ipaddr, port=port['port'], 33 | protocol=port['transport']) 34 | except KeyError: 35 | self.insert_ports(ip_address=ipaddr, port=port['port'], protocol=port['transport']) 36 | except shodan.exception.APIError: 37 | pass 38 | 39 | time.sleep(limit) 40 | -------------------------------------------------------------------------------- /modules/recon/repositories-vulnerabilities/github_dorks.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.github import GithubMixin 3 | import os 4 | 5 | class Module(BaseModule, GithubMixin): 6 | meta = { 7 | 'name': 'Github Dork Analyzer', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.0', 10 | 'description': 'Uses the Github API to search for possible vulnerabilites in source code by leveraging Github Dorks and the \'repo\' search operator. Updates the \'vulnerabilities\' table with the results.', 11 | 'required_keys': ['github_api'], 12 | 'query': "SELECT DISTINCT owner || '/' || name FROM repositories WHERE name IS NOT NULL AND resource LIKE 'Github' AND category LIKE 'repo'", 13 | 'options': ( 14 | ('dorks', os.path.join(BaseModule.data_path, 'github_dorks.txt'), True, 'file containing a list of Github dorks'), 15 | ), 16 | 'files': ['github_dorks.txt'], 17 | } 18 | 19 | def module_run(self, repos): 20 | with open(self.options['dorks']) as fp: 21 | # create list of dorks and filter out comments 22 | dorks = [x.strip() for x in fp.read().splitlines() if x and not x.startswith('#')] 23 | for repo in repos: 24 | self.heading(repo, level=0) 25 | for dork in dorks: 26 | query = f"repo:{repo} {dork}" 27 | for result in self.search_github_api(query): 28 | data = { 29 | 'reference': query, 30 | 'example': result['html_url'], 31 | 'category': 'Github Dork', 32 | } 33 | self.insert_vulnerabilities(**data) 34 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/resolve.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.resolver import ResolverMixin 3 | import dns.resolver 4 | 5 | class Module(BaseModule, ResolverMixin): 6 | 7 | meta = { 8 | 'name': 'Hostname Resolver', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Resolves the IP address for a host. Updates the \'hosts\' table with the results.', 12 | 'comments': ( 13 | 'Note: Nameserver must be in IP form.', 14 | ), 15 | 'query': 'SELECT DISTINCT host FROM hosts WHERE host IS NOT NULL AND ip_address IS NULL', 16 | } 17 | 18 | def module_run(self, hosts): 19 | q = self.get_resolver() 20 | for host in hosts: 21 | try: 22 | answers = q.query(host) 23 | except dns.resolver.NXDOMAIN: 24 | self.verbose(f"{host} => Unknown") 25 | except dns.resolver.NoAnswer: 26 | self.verbose(f"{host} => No answer") 27 | except (dns.resolver.NoNameservers, dns.resolver.Timeout): 28 | self.verbose(f"{host} => DNS Error") 29 | else: 30 | for i in range(0, len(answers)): 31 | if i == 0: 32 | self.query('UPDATE hosts SET ip_address=? WHERE host=?', (answers[i].address, host)) 33 | else: 34 | data = { 35 | 'host': host, 36 | 'ip_address': answers[i].address 37 | } 38 | self.insert('hosts', data, list(data.keys())) 39 | self.output(f"{host} => {answers[i].address}") 40 | -------------------------------------------------------------------------------- /modules/recon/profiles-profiles/twitter_mentions.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.twitter import TwitterMixin 3 | import re 4 | 5 | class Module(BaseModule, TwitterMixin): 6 | 7 | meta = { 8 | 'name': 'Twitter Mentions', 9 | 'author': 'Robert Frost (@frosty_1313, frosty[at]unluckyfrosty.net)', 10 | 'version': '1.0', 11 | 'description': 'Leverages the Twitter API to enumerate users that were mentioned by the given handle. Updates the \'profiles\' table with the results.', 12 | 'required_keys': ['twitter_api', 'twitter_secret'], 13 | 'comments': ( 14 | 'Twitter limits searchable tweet history to 7 days.', 15 | ), 16 | 'query': "SELECT DISTINCT username FROM profiles WHERE username IS NOT NULL AND resource LIKE 'Twitter' COLLATE NOCASE", 17 | 'options': ( 18 | ('limit', True, True, 'toggle rate limiting'), 19 | ), 20 | } 21 | 22 | def module_run(self, handles): 23 | for handle in handles: 24 | handle = handle if not handle.startswith('@') else handle[1:] 25 | self.heading(handle, level=0) 26 | results = self.search_twitter_api({'q': f"from:{handle}"}, self.options['limit']) 27 | for tweet in results: 28 | if 'entities' in tweet: 29 | if 'user_mentions' in tweet['entities']: 30 | for mention in tweet['entities']['user_mentions']: 31 | handle = mention['screen_name'] 32 | name = mention['name'] 33 | time = tweet['created_at'] 34 | self.insert_profiles(username=handle, resource='Twitter', url='https://twitter.com/' + handle, category='social', notes=name) 35 | -------------------------------------------------------------------------------- /modules/recon/locations-locations/reverse_geocode.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'Reverse Geocoder', 7 | 'author': 'Quentin Kaiser (contact@quentinkaiser.be)', 8 | 'version': '1.0', 9 | 'description': 'Queries the Google Maps API to obtain an address from coordinates.', 10 | 'required_keys': ['google_api'], 11 | 'query': 'SELECT DISTINCT latitude || \',\' || longitude FROM locations WHERE latitude IS NOT NULL AND longitude IS NOT NULL', 12 | } 13 | 14 | def module_run(self, points): 15 | api_key = self.keys.get('google_api') 16 | for point in points: 17 | self.verbose(f"Reverse geocoding ({point})...") 18 | payload = {'latlng' : point, 'key' : api_key} 19 | url = 'https://maps.googleapis.com/maps/api/geocode/json' 20 | resp = self.request('GET', url, params=payload) 21 | # kill the module if nothing is returned 22 | if len(resp.json()['results']) == 0: 23 | self.output(f"Unable to resolve an address for ({point}).") 24 | return 25 | # loop through the results 26 | found = False 27 | for result in resp.json()['results']: 28 | if result['geometry']['location_type'] == 'ROOFTOP': 29 | found = True 30 | lat = point.split(',')[0] 31 | lon = point.split(',')[1] 32 | address = result['formatted_address'] 33 | # store the result 34 | self.insert_locations(lat, lon, address) 35 | if found: 36 | self.query('DELETE FROM locations WHERE latitude=? AND longitude=? AND street_address IS NULL', (lat, lon)) 37 | -------------------------------------------------------------------------------- /modules/reporting/xml.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from dicttoxml import dicttoxml 3 | from xml.dom.minidom import parseString 4 | import codecs 5 | import os 6 | 7 | class Module(BaseModule): 8 | 9 | meta = { 10 | 'name': 'XML Report Generator', 11 | 'author': 'Eric Humphries (@e2fsck) and Tim Tomes (@lanmaster53)', 12 | 'version': '1.1', 13 | 'description': 'Creates an XML report.', 14 | 'options': ( 15 | ('tables', 'hosts, contacts, credentials', True, 'comma delineated list of tables'), 16 | ('filename', os.path.join(BaseModule.workspace, 'results.xml'), True, 'path and filename for report output'), 17 | ), 18 | } 19 | 20 | def module_run(self): 21 | filename = self.options['filename'] 22 | with codecs.open(filename, 'wb', encoding='utf-8') as outfile: 23 | # build a list of table names 24 | tables = [x.strip() for x in self.options['tables'].split(',')] 25 | data_dict = {} 26 | cnt = 0 27 | for table in tables: 28 | data_dict[table] = [] 29 | columns = [x[0] for x in self.get_columns(table)] 30 | columns_str = '", "'.join(columns) 31 | rows = self.query(f'SELECT "{columns_str}" FROM "{table}" ORDER BY 1') 32 | for row in rows: 33 | row_dict = {} 34 | for i in range(0, len(columns)): 35 | row_dict[columns[i]] = row[i] 36 | data_dict[table].append(row_dict) 37 | cnt += 1 38 | # write the xml to a file 39 | reparsed = parseString(dicttoxml(data_dict)) 40 | outfile.write(reparsed.toprettyxml(indent=' '*4)) 41 | self.output(f"{cnt} records added to '{filename}'.") 42 | -------------------------------------------------------------------------------- /modules/recon/domains-contacts/pen.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.utils.parsers import parse_name 3 | import re 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'IANA Private Enterprise Number Contact-by-Domain Lookup', 9 | 'author': 'Jonathan M. Wilbur ', 10 | 'version': '1.1', 11 | 'description': 'Given a domain, finds contacts in the IANA Private Enterprise Number (PEN) registry and adds them to the \'contacts\' table.', 12 | 'required_keys': [], 13 | 'comments': (), 14 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 15 | 'options': (), 16 | } 17 | 18 | def module_run(self, domains): 19 | url = 'https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers' 20 | resp = self.request('GET', url) 21 | if resp.status_code != 200: 22 | self.alert('When retrieving IANA PEN Registry, got HTTP status code ' + str(resp.status_code) + '!') 23 | for domain in domains: 24 | dom = re.escape(domain) 25 | regex = r'(\d+)\s*\n\s{2}(.*)\s*\n\s{4}(.*)\s*\n\s{6}(.*)&' + dom + r'\s*\n' 26 | matchfound = False 27 | for match in re.finditer(regex, resp.text, re.IGNORECASE): 28 | fullname = match.groups()[2] 29 | fname, mname, lname = parse_name(fullname) 30 | email = match.groups()[3] + '@' + domain 31 | self.insert_contacts( 32 | first_name=fname, 33 | middle_name=mname, 34 | last_name=lname, 35 | email=email 36 | ) 37 | matchfound = True 38 | if not matchfound: 39 | self.alert('No matches found for domain \'' + domain + '\'') 40 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/spyse_subdomains.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Spyse Subdomain lookup', 8 | 'author': 'Ryan Hays', 9 | 'version': '1.1', 10 | 'description': 'Uses the Spyse API to discover subdomains.', 11 | 'required_keys': ['spyse_api'], 12 | 'options': ( 13 | ('limit', 100, True, 'Limit the number of results returned. Max is 100.'), 14 | ), 15 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL' 16 | } 17 | 18 | def module_run(self, domains): 19 | key = self.keys.get('spyse_api') 20 | for domain in domains: 21 | self.heading(domain, level=0) 22 | offset = 0 23 | total_ans = 1 24 | while offset < total_ans: 25 | resp = self.request('GET', f"https://api.spyse.com/v3/data/domain/subdomain?offset={offset}&" 26 | f"limit={self.options['limit']}&domain={domain}", 27 | headers={"accept": "application/json", 28 | "Authorization": f"Bearer {key}"}) 29 | 30 | if resp.status_code == 200: 31 | data = resp.json() 32 | total_ans = data['data']['max_view_count'] 33 | 34 | for subdomain in data['data']['items']: 35 | offset += 1 36 | 37 | if subdomain['dns_records']['A'] is not None: 38 | self.insert_hosts(host=subdomain['name'], ip_address=subdomain['dns_records']['A'][0]['ip']) 39 | else: 40 | self.insert_hosts(host=subdomain['name']) 41 | else: 42 | break 43 | -------------------------------------------------------------------------------- /modules/reporting/csv.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import csv 3 | import os 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'CSV File Creator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Creates a CSV file containing the specified harvested data.', 12 | 'options': ( 13 | ('table', 'hosts', True, 'source table of data to export'), 14 | ('filename', os.path.join(BaseModule.workspace, 'results.csv'), True, 'path and filename for output'), 15 | ('headers', False, True, 'include column headers'), 16 | ), 17 | } 18 | 19 | def module_run(self): 20 | badcharacters = ['@', '-', '=', '+'] 21 | filename = self.options['filename'] 22 | # codecs module not used because the csv module converts to ASCII 23 | with open(filename, 'w') as outfile: 24 | table = self.options['table'] 25 | csvwriter = csv.writer(outfile, quoting=csv.QUOTE_ALL) 26 | if self.options['headers']: 27 | columns = [c[0] for c in self.get_columns(table)] 28 | csvwriter.writerow(columns) 29 | cnt = 0 30 | rows = self.query(f'SELECT * FROM "{table}" ORDER BY 1') 31 | for row in rows: 32 | row = [x if x else '' for x in row] 33 | if any(row): 34 | cnt += 1 35 | # prevent csv injection 36 | sanitized_row = [] 37 | for cell in row: 38 | if cell and cell[0] in badcharacters: 39 | cell = ' '+cell 40 | sanitized_row.append(cell) 41 | csvwriter.writerow(sanitized_row) 42 | self.output(f"{cnt} records added to '{filename}'.") 43 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/reverse_resolve.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.resolver import ResolverMixin 3 | import dns.resolver 4 | import dns.reversename 5 | 6 | class Module(BaseModule, ResolverMixin): 7 | 8 | meta = { 9 | 'name': 'Reverse Resolver', 10 | 'author': 'John Babio (@3vi1john), @vulp1n3, and Tim Tomes (@lanmaster53)', 11 | 'version': '1.0', 12 | 'description': 'Conducts a reverse lookup for each IP address to resolve the hostname. Updates the \'hosts\' table with the results.', 13 | 'query': 'SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT NULL', 14 | } 15 | 16 | def module_run(self, addresses): 17 | max_attempts = 3 18 | resolver = self.get_resolver() 19 | for address in addresses: 20 | attempt = 0 21 | while attempt < max_attempts: 22 | try: 23 | addr = dns.reversename.from_address(address) 24 | hosts = resolver.query(addr, 'PTR') 25 | except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): 26 | self.verbose(f"{address} => No record found.") 27 | except dns.resolver.Timeout: 28 | self.verbose(f"{address} => Request timed out.") 29 | attempt += 1 30 | continue 31 | except dns.resolver.NoNameservers: 32 | self.verbose(f"{address} => Invalid nameserver.") 33 | #self.error('Invalid nameserver.') 34 | #return 35 | else: 36 | for host in hosts: 37 | host = str(host)[:-1] # slice the trailing dot 38 | self.insert_hosts(host, address) 39 | # break out of the loop 40 | attempt = max_attempts 41 | -------------------------------------------------------------------------------- /modules/recon/contacts-credentials/hibp_breach.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from urllib.parse import quote_plus 3 | import time 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'Have I been pwned? Breach Search', 9 | 'author': 'Tim Tomes (@lanmaster53), Tyler Halfpop (@tylerhalfpop) and Geoff Pamerleau (@_geoff_p_)', 10 | 'version': '1.2', 11 | 'description': 'Leverages the haveibeenpwned.com API to determine if email addresses are associated with ' 12 | 'breached credentials. Adds compromised email addresses to the \'credentials\' table.', 13 | 'comments': ( 14 | 'The API is rate limited to 1 request per 1.5 seconds.', 15 | ), 16 | 'required_keys': ['hibp_api'], 17 | 'query': 'SELECT DISTINCT email FROM contacts WHERE email IS NOT NULL', 18 | } 19 | 20 | def module_run(self, accounts): 21 | # retrieve status 22 | headers = {'hibp-api-key': self.keys['hibp_api']} 23 | base_url = 'https://haveibeenpwned.com/api/v3/{}/{}?truncateResponse=false' 24 | endpoint = 'breachedaccount' 25 | for account in accounts: 26 | resp = self.request('GET', base_url.format(endpoint, quote_plus(account)), headers=headers) 27 | rcode = resp.status_code 28 | if rcode == 404: 29 | self.verbose(f"{account} => Not Found.") 30 | elif rcode == 400: 31 | self.error(f"{account} => Bad Request.") 32 | continue 33 | else: 34 | for breach in resp.json(): 35 | self.alert(f"{account} => Breach found! Seen in the {breach['Name']} breach that occurred on " 36 | f"{breach['BreachDate']}.") 37 | self.insert_credentials(username=account, leak=breach['Name']) 38 | time.sleep(1.6) 39 | -------------------------------------------------------------------------------- /modules/recon/contacts-domains/censys_email_to_domains.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Domains by Email", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves domains from the TLS certificates for an email address." 14 | " This module queries the 'parsed.subject.email_address' field and" 15 | " updates the 'domains' table with the results." 16 | ), 17 | "query": "SELECT DISTINCT email FROM contacts WHERE email IS NOT NULL", 18 | "options": [ 19 | ("num_buckets", 100, True, "maximum number of buckets to retrieve") 20 | ], 21 | "required_keys": ["censysio_id", "censysio_secret"], 22 | "dependencies": ["censys>=2.1.2"], 23 | } 24 | 25 | def module_run(self, emails): 26 | api_id = self.get_key("censysio_id") 27 | api_secret = self.get_key("censysio_secret") 28 | c = CensysHosts(api_id, api_secret) 29 | for email in emails: 30 | email = email.strip('"') 31 | self.heading(email, level=0) 32 | try: 33 | report = c.aggregate( 34 | f'services.tls.certificates.leaf_data.subject.email_address:"{email}"', 35 | field="services.tls.certificates.leaf_data.names", 36 | num_buckets=self.options.get("num_buckets", 100), 37 | ) 38 | except CensysException: 39 | self.print_exception() 40 | continue 41 | for bucket in report.get("buckets", []): 42 | domain = bucket.get("key") 43 | self.insert_domains(domain=domain, notes=f"Email: {email}") 44 | -------------------------------------------------------------------------------- /modules/recon/profiles-profiles/namechk.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.threads import ThreadingMixin 3 | 4 | 5 | class Module(BaseModule, ThreadingMixin): 6 | 7 | meta = { 8 | 'name': 'NameChk.com Username Validator', 9 | 'author': 'Tim Tomes (@lanmaster53), thrapt (thrapt@gmail.com) and Ryan Hays (@_ryanhays)', 10 | 'version': '1.0', 11 | 'description': 'Leverages NameChk.com API to validate the existance of usernames on specific web sites and ' 12 | 'updates the \'profiles\' table with the results.', 13 | 'required_keys': ['namechk_api'], 14 | 'query': 'SELECT DISTINCT username FROM profiles WHERE username IS NOT NULL', 15 | } 16 | 17 | def module_run(self, usernames): 18 | key = self.keys.get('namechk_api') 19 | headers = {'authorization': f"Bearer {key}", 'Accept': 'application/vnd.api.v1+json'} 20 | # Gets a list of available services 21 | avail_sites = self.request('GET', 'https://api.namechk.com/services/available.json', headers=headers) 22 | if avail_sites.status_code == 200: 23 | for username in usernames: 24 | self.heading(username, level=0) 25 | self.thread(avail_sites.json(), username, headers) 26 | 27 | def module_thread(self, site, username, headers): 28 | payload = {'site': site['short_name'], 'username': username} 29 | resp = self.request('POST', 'https://api.namechk.com/services/check.json', data=payload, headers=headers) 30 | if resp.status_code == 200: 31 | if not resp.json().get('available'): 32 | self.insert_profiles(username=username, resource=site['name'], url=resp.json().get('callback_url'), 33 | category=site['category']) 34 | self.query('DELETE FROM profiles WHERE username = ? and url IS NULL', (username,)) 35 | -------------------------------------------------------------------------------- /modules/recon/companies-contacts/pen.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.utils.parsers import parse_name 3 | import re 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'IANA Private Enterprise Number Contact Getter', 9 | 'author': 'Jonathan M. Wilbur ', 10 | 'version': '1.1', 11 | 'description': 'Given a company name, gathers the registered IANA Private Enterprise Number (PEN) contact from the PEN registry and adds the contacts\'s full name and email address to the \'contacts\' table.', 12 | 'required_keys': [], 13 | 'comments': (), 14 | 'query': 'SELECT DISTINCT company FROM companies WHERE company IS NOT NULL', 15 | 'options': (), 16 | } 17 | 18 | def module_run(self, companies): 19 | url = 'https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers' 20 | resp = self.request('GET', url) 21 | if resp.status_code != 200: 22 | self.alert('When retrieving IANA PEN Registry, got HTTP status code ' + str(resp.status_code) + '!') 23 | for company in companies: 24 | comp = re.escape(company) 25 | regex = r'(\d+)\s*\n\s{2}.*' + comp + r'.*\s*\n\s{4}(.*)\s*\n\s{6}(.*)\s*\n' 26 | matchfound = False 27 | for match in re.finditer(regex, resp.text, re.IGNORECASE): 28 | fullname = match.groups()[1] 29 | fname, mname, lname = parse_name(fullname) 30 | email = match.groups()[2].replace('&', '@') 31 | self.insert_contacts( 32 | first_name=fname, 33 | middle_name=mname, 34 | last_name=lname, 35 | email=email 36 | ) 37 | matchfound = True 38 | if not matchfound: 39 | self.alert('No matches found for company \'' + company + '\'') 40 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/censys_ip.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from recon.core.module import BaseModule 4 | 5 | from censys.search import CensysHosts 6 | from censys.common.exceptions import CensysException 7 | 8 | 9 | def grouper(n, iterable): 10 | # via https://stackoverflow.com/a/8991553 11 | it = iter(iterable) 12 | while True: 13 | chunk = tuple(itertools.islice(it, n)) 14 | if not chunk: 15 | return 16 | yield chunk 17 | 18 | 19 | class Module(BaseModule): 20 | meta = { 21 | "name": "Censys - Ports by IP", 22 | "author": "Censys, Inc. ", 23 | "version": 2.1, 24 | "description": ( 25 | "Retrieves the open ports for each IP address. " 26 | "Updates the 'ports' table with the results." 27 | ), 28 | "query": ( 29 | "SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT" 30 | " NULL" 31 | ), 32 | "required_keys": ["censysio_id", "censysio_secret"], 33 | "dependencies": ["censys>=2.1.2"], 34 | } 35 | 36 | def module_run(self, hosts): 37 | api_id = self.get_key("censysio_id") 38 | api_secret = self.get_key("censysio_secret") 39 | c = CensysHosts(api_id, api_secret) 40 | for ip in hosts: 41 | ip = ip.strip('"') 42 | self.heading(ip, level=0) 43 | try: 44 | host = c.view(ip) 45 | except CensysException: 46 | self.print_exception() 47 | continue 48 | for service in host.get("services", []): 49 | self.insert_ports( 50 | ip_address=ip, 51 | port=service["port"], 52 | protocol=service["transport_protocol"], 53 | banner=service.get("banner"), 54 | notes=service["service_name"], 55 | ) 56 | -------------------------------------------------------------------------------- /modules/recon/netblocks-companies/censys_netblock_company.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Companies by Netblock", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves organizations for a company's netblocks. " 14 | "Updates the 'companies' table with the results." 15 | ), 16 | "query": ( 17 | "SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT" 18 | " NULL" 19 | ), 20 | "options": [ 21 | ( 22 | "num_buckets", 23 | "100", 24 | False, 25 | "maximum number of buckets to retrieve", 26 | ) 27 | ], 28 | "required_keys": ["censysio_id", "censysio_secret"], 29 | "dependencies": ["censys>=2.1.2"], 30 | } 31 | 32 | def module_run(self, netblocks): 33 | api_id = self.get_key("censysio_id") 34 | api_secret = self.get_key("censysio_secret") 35 | c = CensysHosts(api_id, api_secret) 36 | for netblock in netblocks: 37 | self.heading(netblock, level=0) 38 | try: 39 | # we only need one per netblock since they'll all have the same by ASN 40 | report = c.aggregate( 41 | f"ip:{netblock}", 42 | fields="autonomous_system.name", 43 | num_buckets=int(self.options.get("NUM_BUCKETS", "100")), 44 | ) 45 | except CensysException: 46 | self.print_exception() 47 | continue 48 | for bucket in report.get("buckets", []): 49 | company = bucket.get("key") 50 | self.insert_companies(company=company) 51 | -------------------------------------------------------------------------------- /modules/recon/repositories-vulnerabilities/gists_search.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import os 3 | 4 | class Module(BaseModule): 5 | meta = { 6 | 'name': 'Github Gist Searcher', 7 | 'author': 'Tim Tomes (@lanmaster53)', 8 | 'version': '1.0', 9 | 'description': 'Uses the Github API to download and search Gists for possible information disclosures. Updates the \'vulnerabilities\' table with the results.', 10 | 'comments': ( 11 | 'Gist searches are case sensitive. Include all desired permutations in the keyword list.', 12 | ), 13 | 'query': "SELECT DISTINCT url FROM repositories WHERE url IS NOT NULL AND resource LIKE 'Github' AND category LIKE 'gist'", 14 | 'options': ( 15 | ('keywords', os.path.join(BaseModule.data_path, 'gist_keywords.txt'), True, 'file containing a list of keywords'), 16 | ), 17 | 'files': ['gist_keywords.txt'], 18 | } 19 | 20 | def module_run(self, gists): 21 | with open(self.options['keywords']) as fp: 22 | # create list of keywords and filter out comments 23 | keywords = [x.strip() for x in fp.read().splitlines() if x and not x.startswith('#')] 24 | for gist in gists: 25 | filename = gist.split(os.sep)[-1] 26 | self.heading(filename, level=0) 27 | resp = self.request('GET', gist) 28 | for keyword in keywords: 29 | self.verbose(f"Searching Gist for: {keyword}") 30 | lines = resp.text.splitlines() 31 | for lineno, line in enumerate(lines): 32 | if keyword in line: 33 | data = { 34 | 'reference': gist, 35 | 'example': f"line {lineno}: {line.strip()}", 36 | 'category': 'Information Disclosure', 37 | } 38 | self.insert_vulnerabilities(**data) 39 | -------------------------------------------------------------------------------- /modules/recon/netblocks-hosts/reverse_resolve.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.resolver import ResolverMixin 3 | import dns.resolver 4 | import dns.reversename 5 | 6 | class Module(BaseModule, ResolverMixin): 7 | 8 | meta = { 9 | 'name': 'Reverse Resolver', 10 | 'author': 'John Babio (@3vi1john)', 11 | 'version': '1.0', 12 | 'description': 'Conducts a reverse lookup for each of a netblock\'s IP addresses to resolve the hostname. Updates the \'hosts\' table with the results.', 13 | 'query': 'SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT NULL', 14 | } 15 | 16 | def module_run(self, netblocks): 17 | max_attempts = 3 18 | resolver = self.get_resolver() 19 | for netblock in netblocks: 20 | self.heading(netblock, level=0) 21 | addresses = self.cidr_to_list(netblock) 22 | for address in addresses: 23 | attempt = 0 24 | while attempt < max_attempts: 25 | try: 26 | addr = dns.reversename.from_address(address) 27 | hosts = resolver.query(addr,'PTR') 28 | except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): 29 | self.verbose(f"{address} => No record found.") 30 | except dns.resolver.Timeout: 31 | self.verbose(f"{address} => Request timed out.") 32 | attempt += 1 33 | continue 34 | except (dns.resolver.NoNameservers): 35 | self.verbose(f"{address} => Invalid nameserver.") 36 | else: 37 | for host in hosts: 38 | host = str(host)[:-1] # slice the trailing dot 39 | self.insert_hosts(host, address) 40 | # break out of the loop 41 | attempt = max_attempts 42 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/bing_ip.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.search import BingAPIMixin 3 | from recon.utils.parsers import parse_hostname 4 | import re 5 | 6 | class Module(BaseModule, BingAPIMixin): 7 | 8 | meta = { 9 | 'name': 'Bing API IP Neighbor Enumerator', 10 | 'author': 'Tim Tomes (@lanmaster53)', 11 | 'version': '1.0', 12 | 'description': 'Leverages the Bing API and "ip:" advanced search operator to enumerate other virtual hosts sharing the same IP address. Updates the \'hosts\' table with the results.', 13 | 'required_keys': ['bing_api'], 14 | 'comments': ( 15 | 'This module only stores hosts whose domain matches an entry in the domains table.', 16 | ), 17 | 'query': 'SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT NULL', 18 | 'options': ( 19 | ('restrict', True, True, 'restrict added hosts to current domains'), 20 | ), 21 | } 22 | 23 | def module_run(self, addresses): 24 | # build a regex that matches any of the stored domains 25 | domains = [x[0] for x in self.query('SELECT DISTINCT domain from domains WHERE domain IS NOT NULL')] 26 | domains_str = '|'.join([r'\.'+re.escape(x)+'$' for x in domains]) 27 | regex = f"(?:{domains_str})" 28 | for address in addresses: 29 | self.heading(address, level=0) 30 | query = f"ip:{address}" 31 | results = self.search_bing_api(query) 32 | if not results: 33 | self.verbose(f"No additional hosts discovered at '{address}'.") 34 | for result in results: 35 | host = parse_hostname(result['displayUrl']) 36 | self.verbose(host) 37 | # apply restriction 38 | if self.options['restrict'] and not re.search(regex, host): 39 | continue 40 | # add hosts to the database 41 | self.insert_hosts(host, address) 42 | -------------------------------------------------------------------------------- /modules/recon/companies-multi/github_miner.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.github import GithubMixin 3 | from urllib.parse import quote_plus 4 | 5 | class Module(BaseModule, GithubMixin): 6 | meta = { 7 | 'name': 'Github Resource Miner', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.1', 10 | 'description': 'Uses the Github API to enumerate repositories and member profiles associated with a company search string. Updates the respective tables with the results.', 11 | 'required_keys': ['github_api'], 12 | 'query': 'SELECT DISTINCT company FROM companies WHERE company IS NOT NULL', 13 | 'options': ( 14 | ('ignoreforks', True, True, 'ignore forks'), 15 | ), 16 | } 17 | 18 | def module_run(self, companies): 19 | for company in companies: 20 | self.heading(company, level=0) 21 | # enumerate members 22 | members = self.query_github_api(f"/orgs/{quote_plus(company)}/members") 23 | for member in members: 24 | data = { 25 | 'username': member['login'], 26 | 'url': member['html_url'], 27 | 'notes': company, 28 | 'resource': 'Github', 29 | 'category': 'coding', 30 | } 31 | self.insert_profiles(**data) 32 | # enumerate repositories 33 | repos = self.query_github_api(f"/orgs/{quote_plus(company)}/repos") 34 | for repo in repos: 35 | if self.options['ignoreforks'] and repo['fork']: 36 | continue 37 | data = { 38 | 'name': repo['name'], 39 | 'owner': repo['owner']['login'], 40 | 'description': repo['description'], 41 | 'url': repo['html_url'], 42 | 'resource': 'Github', 43 | 'category': 'repo', 44 | } 45 | self.insert_repositories(**data) 46 | -------------------------------------------------------------------------------- /modules/recon/domains-vulnerabilities/xssed.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from datetime import datetime 3 | import re 4 | import time 5 | 6 | class Module(BaseModule): 7 | 8 | meta = { 9 | 'name': 'XSSed Domain Lookup', 10 | 'author': 'Micah Hoffman (@WebBreacher)', 11 | 'version': '1.1', 12 | 'description': 'Checks XSSed.com for XSS records associated with a domain and displays the first 20 results.', 13 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 14 | } 15 | 16 | def module_run(self, domains): 17 | url = 'http://xssed.com/search?key=%s' 18 | url_vuln = 'http://xssed.com/mirror/%s/' 19 | for domain in domains: 20 | self.heading(domain, level=0) 21 | resp = self.request('GET', url % domain) 22 | vulns = re.findall('mirror/([0-9]+)/\' target=\'_blank\'>', resp.text) 23 | for vuln in vulns: 24 | # Go fetch and parse the specific page for this item 25 | resp_vuln = self.request('GET', url_vuln % vuln) 26 | # Parse the response and get the details 27 | details = re.findall(r']*>[^:?]+[:?]+(.+?)<\/th>', resp_vuln.text)#.replace(' ', ' ')) 28 | details = [self.html_unescape(x).strip() for x in details] 29 | if not re.match(rf"(^|.*\.){re.escape(domain)}$", details[5], re.IGNORECASE): 30 | continue 31 | data = {} 32 | data['host'] = details[5] 33 | data['reference'] = url_vuln % vuln 34 | data['publish_date'] = datetime.strptime(details[1], '%d/%m/%Y') 35 | data['category'] = details[6] 36 | data['status'] = re.search(r'([UNFIXED]+)',details[3]).group(1).lower() 37 | data['example'] = details[8] 38 | self.insert_vulnerabilities(**data) 39 | # results in 503 errors if not throttled 40 | time.sleep(1) 41 | if not vulns: 42 | self.output('No vulnerabilites found.') 43 | -------------------------------------------------------------------------------- /modules/recon/locations-pushpins/twitter.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.twitter import TwitterMixin 3 | from datetime import datetime 4 | 5 | class Module(BaseModule, TwitterMixin): 6 | 7 | meta = { 8 | 'name': 'Twitter Geolocation Search', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.1', 11 | 'description': 'Searches Twitter for media in the specified proximity to a location.', 12 | 'required_keys': ['twitter_api', 'twitter_secret'], 13 | 'query': 'SELECT DISTINCT latitude || \',\' || longitude FROM locations WHERE latitude IS NOT NULL AND longitude IS NOT NULL', 14 | 'options': ( 15 | ('radius', 1, True, 'radius in kilometers'), 16 | ), 17 | } 18 | 19 | def module_run(self, points): 20 | rad = self.options['radius'] 21 | for point in points: 22 | self.heading(point, level=0) 23 | self.output('Collecting data for an unknown number of tweets...') 24 | results = self.search_twitter_api({'q':'', 'geocode': f"{point},{rad}km", 'count':'100'}) 25 | for tweet in results: 26 | if not tweet['geo']: 27 | continue 28 | tweet_id = tweet['id_str'] 29 | source = 'Twitter' 30 | screen_name = tweet['user']['screen_name'] 31 | profile_name = tweet['user']['name'] 32 | profile_url = f"https://twitter.com/{screen_name}" 33 | media_url = f"https://twitter.com/{screen_name}/statuses/{tweet_id}" 34 | thumb_url = tweet['user']['profile_image_url_https'] 35 | message = tweet['text'] 36 | latitude = tweet['geo']['coordinates'][0] 37 | longitude = tweet['geo']['coordinates'][1] 38 | time = datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S +0000 %Y') 39 | self.insert_pushpins(source, screen_name, profile_name, profile_url, media_url, thumb_url, message, latitude, longitude, time) 40 | self.verbose(f"{len(results)} tweets processed.") 41 | -------------------------------------------------------------------------------- /modules/recon/netblocks-hosts/shodan_net.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import shodan 3 | import time 4 | import re 5 | 6 | 7 | class Module(BaseModule): 8 | 9 | meta = { 10 | 'name': 'Shodan Network Enumerator', 11 | 'author': 'Mike Siegel and Tim Tomes (@lanmaster53) & Ryan Hays (@_ryanhays)', 12 | 'version': '1.2', 13 | 'description': 'Harvests hosts from the Shodan API by using the \'net\' search operator. Updates the \'hosts\' ' 14 | 'table with the results.', 15 | 'required_keys': ['shodan_api'], 16 | 'query': 'SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT NULL', 17 | 'options': ( 18 | ('limit', 1, True, 'limit number of api requests per input source (0 = unlimited)'), 19 | ), 20 | 'dependencies': ['shodan'] 21 | } 22 | 23 | def module_run(self, netblocks): 24 | limit = self.options['limit'] 25 | api = shodan.Shodan(self.keys.get('shodan_api')) 26 | 27 | for netblock in netblocks: 28 | self.heading(netblock, level=0) 29 | query = f"net:{netblock}" 30 | 31 | page = 1 32 | rec_count = 0 33 | total_results = 1 34 | 35 | while rec_count < total_results: 36 | results = api.search(query, page=page) 37 | total_results = results['total'] 38 | 39 | for host in results['matches']: 40 | rec_count += 1 41 | 42 | if len(host['hostnames']) > 0: 43 | self.insert_ports(host=host['hostnames'][0], ip_address=host['ip_str'], port=host['port'], 44 | protocol=host['transport']) 45 | self.insert_hosts(host=host['hostnames'][0], ip_address=host['ip_str']) 46 | else: 47 | self.insert_ports(ip_address=host['ip_str'], port=host['port'], protocol=host['transport']) 48 | self.insert_hosts(ip_address=host['ip_str']) 49 | 50 | page += 1 51 | time.sleep(limit) 52 | -------------------------------------------------------------------------------- /modules/recon/credentials-credentials/hashes_org.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import io 3 | import time 4 | import xml.etree.ElementTree 5 | 6 | class Module(BaseModule): 7 | 8 | meta = { 9 | 'name': 'Hashes.org Hash Lookup', 10 | 'author': 'Tim Tomes (@lanmaster53) and Mike Lisi (@MikeCodesThings)', 11 | 'version': '1.0', 12 | 'description': 'Uses the Hashes.org API to perform a reverse hash lookup. Updates the \'credentials\' table with the positive results.', 13 | 'required_keys': ['hashes_api'], 14 | 'comments': ( 15 | 'Hash types supported: MD5, MD4, NTLM, LM, DOUBLEMD5, TRIPLEMD5, MD5SHA1, SHA1, MYSQL5, SHA1MD5, DOUBLESHA1, RIPEMD160', 16 | 'Hashes.org is a free service. Please consider a small donation to keep the service running. Thanks. - @s3inlc' 17 | ), 18 | 'query': 'SELECT DISTINCT hash FROM credentials WHERE hash IS NOT NULL AND password IS NULL AND type IS NOT \'Adobe\'', 19 | } 20 | 21 | def module_run(self, hashes): 22 | api_key = self.keys.get('hashes_api') 23 | for hashstr in hashes: 24 | url = f"https://hashes.org/api.php?key={api_key}&query={hashstr}" 25 | # 20 requests per minute 26 | time.sleep(3) 27 | resp = self.request('GET', url) 28 | if resp.status_code != 200: 29 | self.error(f"Unexpected service response: {resp.status_code}") 30 | break 31 | elif resp.json()['status'] == 'error': 32 | self.error(resp.json()['errorMessage']) 33 | break 34 | for result in resp.json()['result']: 35 | if resp.json()['result'][result]: 36 | plaintext = resp.json()['result'][result]['plain'] 37 | hashtype = resp.json()['result'][result]['algorithm'] 38 | self.alert(f"{hashstr} ({hashtype}) => {plaintext}") 39 | self.query('UPDATE credentials SET password=?, type=? WHERE hash=?', (plaintext, hashtype, hashstr)) 40 | else: 41 | self.verbose(f"{hashstr} => {'Not found.'}") 42 | -------------------------------------------------------------------------------- /modules/recon/repositories-profiles/github_commits.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.github import GithubMixin 3 | from recon.utils.parsers import parse_name 4 | from urllib.parse import quote_plus 5 | 6 | class Module(BaseModule, GithubMixin): 7 | meta = { 8 | 'name': 'Github Commit Searcher', 9 | 'author': 'Michael Henriksen (@michenriksen)', 10 | 'version': '1.0', 11 | 'description': 'Uses the Github API to gather user profiles from repository commits. Updates the \'profiles\' table with the results.', 12 | 'required_keys': ['github_api'], 13 | 'query': "SELECT DISTINCT owner, name FROM repositories WHERE resource LIKE 'Github' AND category LIKE 'repo'", 14 | 'options': ( 15 | ('maxpages', 1, True, 'maximum number of commit pages to process for each repository (0 = unlimited)'), 16 | ('author', True, True, 'extract author information'), 17 | ('committer', True, True, 'extract committer information'), 18 | ), 19 | } 20 | 21 | def module_run(self, repos): 22 | for repo in repos: 23 | commits = self.query_github_api( 24 | endpoint=f"/repos/{quote_plus(repo[0])}/{quote_plus(repo[1])}/commits", 25 | payload={}, 26 | options={'max_pages': int(self.options['maxpages']) or None}, 27 | ) 28 | for commit in commits: 29 | for key in ('committer', 'author'): 30 | if self.options[key] and key in commit and commit[key]: 31 | url = commit[key]['html_url'] 32 | login = commit[key]['login'] 33 | self.insert_profiles(username=login, url=url, resource='Github', category='coding') 34 | if self.options[key] and key in commit['commit'] and commit['commit'][key]: 35 | name = commit['commit'][key]['name'] 36 | email = commit['commit'][key]['email'] 37 | fname, mname, lname = parse_name(name) 38 | self.insert_contacts(first_name=fname, middle_name=mname, last_name=lname, email=email, title='Github Contributor') 39 | -------------------------------------------------------------------------------- /modules/recon/companies-multi/shodan_org.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import shodan 3 | import time 4 | 5 | 6 | class Module(BaseModule): 7 | meta = { 8 | 'name': 'Shodan IP Enumerator', 9 | 'author': 'Austin Tipton (@hiEntropy404) & Ryan Hays (@_ryanhays)', 10 | 'version': '1.1', 11 | 'description': 'Harvests host and port information from the Shodan API by using the \'org\' search operator. ' 12 | 'Updates the \'hosts\' and \'ports\' tables with the results.', 13 | 'required_keys': ['shodan_api'], 14 | 'query': 'SELECT DISTINCT company FROM companies WHERE company IS NOT NULL', 15 | 'options': ( 16 | ('limit', 1, True, 'limit number of api requests per input source (0 = unlimited)'), 17 | ), 18 | 'dependencies': ['shodan'] 19 | } 20 | 21 | def module_run(self, companies): 22 | limit = self.options['limit'] 23 | api = shodan.Shodan(self.keys.get('shodan_api')) 24 | 25 | for company in companies: 26 | self.heading(company, level=0) 27 | query = f"org:\"{company}\"" 28 | try: 29 | page = 1 30 | rec_count = 0 31 | total_results = 1 32 | while rec_count < total_results: 33 | results = api.search(query, page=page) 34 | total_results = results['total'] 35 | for port in results['matches']: 36 | rec_count += 1 37 | try: 38 | for hostname in port['hostnames']: 39 | self.insert_ports(host=hostname, ip_address=port['ip_str'], port=port['port'], 40 | protocol=port['transport']) 41 | self.insert_hosts(host=hostname, ip_address=port['ip_str']) 42 | except KeyError: 43 | self.insert_ports(ip_address=ipaddr, port=port['port'], protocol=port['transport']) 44 | 45 | page += 1 46 | time.sleep(limit) 47 | 48 | except shodan.exception.APIError: 49 | pass 50 | -------------------------------------------------------------------------------- /modules/recon/companies-contacts/censys_email_address.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Emails by Company", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves email addresses from the TLS certificates for a" 14 | " company. This module queries the" 15 | " 'services.tls.certificates.leaf_data.subject.email_address'" 16 | " field and updates the 'contacts' table with the results." 17 | ), 18 | "query": ( 19 | "SELECT DISTINCT company FROM companies WHERE company IS NOT NULL" 20 | ), 21 | "options": [ 22 | ( 23 | "num_buckets", 24 | "100", 25 | False, 26 | "maximum number of buckets to retrieve", 27 | ) 28 | ], 29 | "required_keys": ["censysio_id", "censysio_secret"], 30 | "dependencies": ["censys>=2.1.2"], 31 | } 32 | 33 | def module_run(self, companies): 34 | api_id = self.get_key("censysio_id") 35 | api_secret = self.get_key("censysio_secret") 36 | c = CensysHosts(api_id, api_secret) 37 | for company in companies: 38 | company = company.strip('"') 39 | self.heading(company, level=0) 40 | try: 41 | report = c.aggregate( 42 | "same_service(services.tls.certificates.leaf_data.subject.email_address:*" 43 | " and " 44 | f'services.tls.certificates.leaf_data.subject.organization:"{company}")', 45 | field="services.tls.certificates.leaf_data.subject.email_address", 46 | num_buckets=int(self.options.get("NUM_BUCKETS", "100")), 47 | ) 48 | except CensysException: 49 | self.print_exception() 50 | continue 51 | for bucket in report.get("buckets", []): 52 | email = bucket.get("key") 53 | self.insert_contacts(email=email) 54 | -------------------------------------------------------------------------------- /modules/recon/domains-companies/censys_companies.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Companies by Domain", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves the TLS certificates for a domain. This module queries" 14 | " the 'services.tls.certificates.leaf_data.names' field and" 15 | " updates the 'companies' table with the results." 16 | ), 17 | "query": ( 18 | "SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL" 19 | ), 20 | "options": [ 21 | ( 22 | "num_buckets", 23 | "100", 24 | False, 25 | "maximum number of buckets to retrieve", 26 | ) 27 | ], 28 | "required_keys": ["censysio_id", "censysio_secret"], 29 | "dependencies": ["censys>=2.1.2"], 30 | } 31 | 32 | def module_run(self, domains): 33 | api_id = self.get_key("censysio_id") 34 | api_secret = self.get_key("censysio_secret") 35 | c = CensysHosts(api_id, api_secret) 36 | for domain in domains: 37 | domain = domain.strip('"') 38 | self.heading(domain, level=0) 39 | try: 40 | report = c.aggregate( 41 | "same_service(services.tls.certificates.leaf_data.names:" 42 | f" {domain} and" 43 | " services.tls.certificates.leaf_data.subject.organization: *)", 44 | field="services.tls.certificates.leaf_data.subject.organization", 45 | num_buckets=int(self.options.get("NUM_BUCKETS", "100")), 46 | ) 47 | except CensysException: 48 | self.print_exception() 49 | continue 50 | for bucket in report.get("buckets", []): 51 | company = bucket.get("key") 52 | self.insert_companies( 53 | company=company, description=f"Domain: {domain}" 54 | ) 55 | -------------------------------------------------------------------------------- /modules/recon/profiles-repositories/github_repos.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.github import GithubMixin 3 | from urllib.parse import quote_plus 4 | 5 | class Module(BaseModule, GithubMixin): 6 | meta = { 7 | 'name': 'Github Code Enumerator', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.1', 10 | 'description': 'Uses the Github API to enumerate repositories and gists owned by a Github user. Updates the \'repositories\' table with the results.', 11 | 'required_keys': ['github_api'], 12 | 'query': "SELECT DISTINCT username FROM profiles WHERE username IS NOT NULL AND resource LIKE 'Github'", 13 | 'options': ( 14 | ('ignoreforks', False, False, 'ignore forks'), 15 | ), 16 | } 17 | 18 | def module_run(self, users): 19 | for user in users: 20 | self.heading(user, level=0) 21 | # enumerate repositories 22 | repos = self.query_github_api(f"/users/{quote_plus(user)}/repos") 23 | for repo in repos: 24 | if self.options['ignoreforks'] and repo['fork']: 25 | continue 26 | data = { 27 | 'name': repo['name'], 28 | 'owner': repo['owner']['login'], 29 | 'description': repo['description'], 30 | 'url': repo['html_url'], 31 | 'resource': 'Github', 32 | 'category': 'repo', 33 | } 34 | self.insert_repositories(**data) 35 | # enumerate gists 36 | gists = self.query_github_api(f"/users/{quote_plus(user)}/gists") 37 | for gist in gists: 38 | files = gist['files'].values() 39 | for _file in files: 40 | data = { 41 | 'name': _file['filename'], 42 | 'owner': gist['owner']['login'], 43 | 'description': gist['description'], 44 | 'url': _file['raw_url'], 45 | 'resource': 'Github', 46 | 'category': 'gist', 47 | } 48 | self.insert_repositories(**data) 49 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/shodan_hostname.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import shodan 3 | import time 4 | import re 5 | 6 | 7 | class Module(BaseModule): 8 | 9 | meta = { 10 | 'name': 'Shodan Hostname Enumerator', 11 | 'author': 'Tim Tomes (@lanmaster53) & Ryan Hays (@_ryanhays)', 12 | 'version': '1.1', 13 | 'description': 'Harvests hosts from the Shodan API by using the \'hostname\' search operator. Updates the ' 14 | '\'hosts\' table with the results.', 15 | 'required_keys': ['shodan_api'], 16 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 17 | 'options': ( 18 | ('limit', 1, True, 'limit number of api requests per input source (0 = unlimited)'), 19 | ), 20 | 'dependencies': ['shodan'] 21 | } 22 | 23 | def module_run(self, domains): 24 | limit = self.options['limit'] 25 | api = shodan.Shodan(self.keys.get('shodan_api')) 26 | 27 | for domain in domains: 28 | self.heading(domain, level=0) 29 | query = f"hostname:{domain}" 30 | 31 | try: 32 | page = 1 33 | rec_count = 0 34 | total_results = 1 35 | while rec_count < total_results: 36 | results = api.search(query, page=page) 37 | total_results = results['total'] 38 | 39 | for host in results['matches']: 40 | rec_count += 1 41 | try: 42 | for hostname in host['hostnames']: 43 | self.insert_ports(host=hostname, ip_address=host['ip_str'], port=host['port'], 44 | protocol=host['transport']) 45 | self.insert_hosts(host=hostname, ip_address=host['ip_str']) 46 | except KeyError: 47 | self.insert_ports(ip_address=ipaddr, port=host['port'], protocol=host['transport']) 48 | self.insert_host(ip_address=host['ip_str']) 49 | 50 | page += 1 51 | time.sleep(limit) 52 | 53 | except shodan.exception.APIError: 54 | pass 55 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/censys_hostname.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Hosts by Hostname", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves all IPs for a given hostname. This module queries the" 14 | " 'name' field and updates the 'hosts' and 'ports' tables with the" 15 | " results." 16 | ), 17 | "query": "SELECT DISTINCT host FROM hosts WHERE host IS NOT NULL", 18 | "required_keys": ["censysio_id", "censysio_secret"], 19 | "dependencies": ["censys>=2.1.2"], 20 | } 21 | 22 | def module_run(self, hosts): 23 | api_id = self.get_key("censysio_id") 24 | api_secret = self.get_key("censysio_secret") 25 | c = CensysHosts(api_id, api_secret) 26 | for host in hosts: 27 | host = host.strip('"') 28 | self.heading(host, level=0) 29 | try: 30 | query = c.search(f"name:{host}", virtual_hosts="ONLY") 31 | except CensysException: 32 | self.print_exception() 33 | continue 34 | for hit in query(): 35 | common_kwargs = { 36 | "ip_address": hit["ip"], 37 | "host": hit.get("name"), 38 | } 39 | location = hit.get("location", {}) 40 | coords = location.get("coordinates", {}) 41 | self.insert_hosts( 42 | region=location.get("continent"), 43 | country=location.get("country"), 44 | latitude=coords.get("latitude"), 45 | longitude=coords.get("longitude"), 46 | **common_kwargs, 47 | ) 48 | for service in hit.get("services", []): 49 | self.insert_ports( 50 | port=service["port"], 51 | protocol=service["transport_protocol"], 52 | notes=service["service_name"], 53 | **common_kwargs, 54 | ) 55 | -------------------------------------------------------------------------------- /modules/recon/companies-domains/censys_subdomains.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysCertificates 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Subdomains by Company", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves subdomains for a company. This module queries the" 14 | " 'parsed.subject.organization' field and updates the 'domains'" 15 | " table with the results." 16 | ), 17 | "query": ( 18 | "SELECT DISTINCT company FROM companies WHERE company IS NOT NULL" 19 | ), 20 | "options": [ 21 | ("max_records", 100, True, "maximum number of records to retrieve") 22 | ], 23 | "required_keys": ["censysio_id", "censysio_secret"], 24 | "dependencies": ["censys>=2.1.2"], 25 | } 26 | 27 | def module_run(self, companies): 28 | api_id = self.get_key("censysio_id") 29 | api_secret = self.get_key("censysio_secret") 30 | c = CensysCertificates(api_id, api_secret) 31 | SEARCH_FIELDS = [ 32 | "parsed.subject.organization", 33 | "parsed.subject.organizational_unit", 34 | ] 35 | for company in companies: 36 | company = company.strip('"') 37 | self.heading(company, level=0) 38 | try: 39 | query = " OR ".join( 40 | ['{0}:"{1}"'.format(x, company) for x in SEARCH_FIELDS] 41 | ) 42 | res = c.search( 43 | query, 44 | ["parsed.names"], 45 | max_records=self.options.get("max_records", 100), 46 | ) 47 | except CensysException: 48 | self.print_exception() 49 | continue 50 | domains = set() 51 | for result in res: 52 | for name in result.get("parsed.names", []): 53 | if name.startswith("*."): 54 | name = name.replace("*.", "") 55 | domains.add(name) 56 | for domain in domains: 57 | self.insert_domains(domain=domain) 58 | -------------------------------------------------------------------------------- /modules/recon/domains-contacts/pgp_search.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.utils.parsers import parse_name 3 | import re 4 | 5 | 6 | class Module(BaseModule): 7 | 8 | meta = { 9 | 'name': 'PGP Key Owner Lookup', 10 | 'author': 'Robert Frost (@frosty_1313, frosty[at]unluckyfrosty.net) and Cam Barts (@cam-barts)', 11 | 'description': 'Searches the MIT public PGP key server for email addresses of the given domain. Updates the \'contacts\' table with the results.', 12 | 'comments': ( 13 | 'Inspiration from theHarvester.py by Christan Martorella: cmarorella[at]edge-seecurity.com', 14 | ), 15 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 16 | 'version': '1.4', 17 | } 18 | 19 | def module_run(self, domains): 20 | url = 'http://pgp.key-server.io/pks/lookup' 21 | for domain in domains: 22 | self.heading(domain, level=0) 23 | payload = {'search': domain} 24 | resp = self.request('GET', url, params=payload) 25 | # split the response into the relevant lines 26 | lines = [x.strip() for x in re.split('[\n<>]', resp.text) if domain in x] 27 | results = [] 28 | for line in lines: 29 | # remove parenthesized items 30 | line = re.sub(r'\s*\(.*\)\s*', '', line) 31 | # parse out name and email address 32 | match = re.search(r'^(.*)<(.*)>$', line) 33 | if match: 34 | # clean up and append the parsed elements 35 | results.append(tuple([x.strip() for x in match.group(1, 2)])) 36 | results = list(set(results)) 37 | if not results: 38 | self.output('No results found.') 39 | continue 40 | for contact in results: 41 | name = contact[0].strip() 42 | fname, mname, lname = parse_name(name) 43 | email = contact[1] 44 | if email.lower().endswith(domain.lower()): 45 | self.insert_contacts( 46 | first_name=fname, 47 | middle_name=mname, 48 | last_name=lname, 49 | email=email 50 | ) 51 | -------------------------------------------------------------------------------- /modules/recon/domains-contacts/whois_pocs.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | class Module(BaseModule): 4 | 5 | meta = { 6 | 'name': 'Whois POC Harvester', 7 | 'author': 'Tim Tomes (@lanmaster53)', 8 | 'version': '1.0', 9 | 'description': 'Uses the ARIN Whois RWS to harvest POC data from whois queries for the given domain. Updates the \'contacts\' table with the results.', 10 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 11 | } 12 | 13 | def module_run(self, domains): 14 | headers = {'Accept': 'application/json'} 15 | for domain in domains: 16 | self.heading(domain, level=0) 17 | url = f"http://whois.arin.net/rest/pocs;domain={domain}" 18 | self.verbose(f"URL: {url}") 19 | resp = self.request('GET', url, headers=headers) 20 | if 'Your search did not yield any results.' in resp.text: 21 | self.output('No contacts found.') 22 | continue 23 | handles = [x['@handle'] for x in resp.json()['pocs']['pocRef']] if type(resp.json()['pocs']['pocRef']) == list else [resp.json()['pocs']['pocRef']['@handle']] 24 | for handle in handles: 25 | url = f"http://whois.arin.net/rest/poc/{handle}" 26 | self.verbose(f"URL: {url}") 27 | resp = self.request('GET', url, headers=headers) 28 | poc = resp.json()['poc'] 29 | emails = poc['emails']['email'] if type(poc['emails']['email']) == list else [poc['emails']['email']] 30 | for email in emails: 31 | fname = poc['firstName']['$'] if 'firstName' in poc else None 32 | lname = poc['lastName']['$'] 33 | name = ' '.join([x for x in [fname, lname] if x]) 34 | email = email['$'] 35 | title = 'Whois contact' 36 | city = poc['city']['$'].title() 37 | state = poc['iso3166-2']['$'].upper() if 'iso3166-2' in poc else None 38 | region = ', '.join([x for x in [city, state] if x]) 39 | country = poc['iso3166-1']['name']['$'].title() 40 | if email.lower().endswith(domain.lower()): 41 | self.insert_contacts(first_name=fname, last_name=lname, email=email, title=title, region=region, country=country) 42 | -------------------------------------------------------------------------------- /modules/recon/ports-hosts/ssl_scan.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import ssl 3 | from socket import setdefaulttimeout, timeout 4 | import re 5 | import cryptography.x509 6 | 7 | 8 | class Module(BaseModule): 9 | 10 | meta = { 11 | 'name': 'SSL Scanner SAN Lookup', 12 | 'author': 'Ryan Hays (@_ryanhays)', 13 | 'version': '1.1', 14 | 'description': 'Queries the ports table to build a list of IP Address:Ports. It then connects to each service ' 15 | 'updating the Ports table with the certificate common name and then adds the Subject Alt Names ' 16 | 'to the hosts table.', 17 | 'query': 'SELECT DISTINCT ("ip_address" || ":" || "port") FROM ports WHERE ip_address IS NOT NULL', 18 | 'dependancies': ['cryptography'] 19 | } 20 | 21 | def module_run(self, hosts): 22 | # https://stackoverflow.com/a/2894918 23 | dn_regex_pat = r'^[a-zA-Z\d-]{,63}(\.[a-zA-Z\d-]{,63})*$' 24 | for host in hosts: 25 | setdefaulttimeout(10) 26 | ip, port = host.split(':') 27 | try: 28 | cert = ssl.get_server_certificate((ip, port), ssl_version=ssl.PROTOCOL_TLS) 29 | except (ssl.SSLError, ConnectionResetError, ConnectionRefusedError, ssl.SSLEOFError, OSError): 30 | self.alert(f"This is not a proper HTTPS service: {ip}:{port}") 31 | continue 32 | except timeout: 33 | self.alert(f"Timed out connecting to host {ip}:{port}") 34 | continue 35 | 36 | x509_cert = cryptography.x509.load_pem_x509_certificate(cert.encode()) 37 | commonnames = x509_cert.subject.get_attributes_for_oid(cryptography.x509.NameOID.COMMON_NAME) 38 | 39 | for cn in commonnames: 40 | if re.match(dn_regex_pat, cn.value): 41 | self.insert_ports(host=cn.value, ip_address=ip, port=port, protocol='tcp') 42 | else: 43 | self.debug(f"Not a valid Common Name: {cn.value}") 44 | 45 | san_ext = x509_cert.extensions.get_extension_for_oid(cryptography.x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value 46 | subaltnames = san_ext.get_values_for_type(cryptography.x509.DNSName) 47 | 48 | for san in subaltnames: 49 | if re.match(dn_regex_pat, san): 50 | self.insert_hosts(host=san) 51 | -------------------------------------------------------------------------------- /modules/recon/profiles-profiles/profiler.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.threads import ThreadingMixin 3 | from urllib.parse import quote_plus 4 | 5 | class Module(BaseModule, ThreadingMixin): 6 | 7 | meta = { 8 | 'name': 'OSINT HUMINT Profile Collector', 9 | 'author': 'Micah Hoffman (@WebBreacher), Brendan Burke (@gbinv)', 10 | 'version': '1.2', 11 | 'description': 'Takes each username from the profiles table and searches a variety of web sites for those users. The list of valid sites comes from the parent project at https://github.com/WebBreacher/WhatsMyName', 12 | 'comments': ( 13 | 'Note: The global timeout option may need to be increased to support slower sites.', 14 | 'Warning: Using this module behind a filtering proxy may cause false negatives as some of these sites may be blocked.', 15 | ), 16 | 'query': 'SELECT DISTINCT username FROM profiles WHERE username IS NOT NULL', 17 | } 18 | 19 | def module_run(self, usernames): 20 | # retrieve list of sites 21 | url = 'https://raw.githubusercontent.com/WebBreacher/WhatsMyName/main/wmn-data.json' 22 | self.verbose(f"Retrieving {'url'}...") 23 | resp = self.request('GET', url) 24 | for user in usernames: 25 | self.heading(f"Looking up data for: {user}") 26 | self.thread(resp.json()['sites'], user) 27 | 28 | def module_thread(self, site, user): 29 | d = dict(site) 30 | if d.get('valid', True) == True: 31 | self.verbose(f"Checking: {d['name']}") 32 | url = d['uri_check'].replace('{account}', quote_plus(user)) 33 | resp = self.request('GET', url, allow_redirects=False) 34 | if resp.status_code == int(d['e_code']): 35 | self.debug(f"Codes matched {resp.status_code} {d['e_code']}") 36 | if d['e_string'] in resp.text or d['e_string'] in resp.headers: 37 | pretty_url = self._pretty_uri(site, user) 38 | self.insert_profiles(username=user, url=pretty_url, resource=d['name'], category=d['cat']) 39 | self.query('DELETE FROM profiles WHERE username = ? and url IS NULL', (user,)) 40 | 41 | def _pretty_uri(self, site, user): 42 | """Return pretty URI for displaying result (in case if API was used to check account)""" 43 | pretty_uri_key = 'uri_pretty' if 'uri_pretty' in site else 'uri_check' 44 | return site[pretty_uri_key].replace('{account}', user) 45 | 46 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/ipinfodb.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import json 3 | import time 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'IPInfoDB GeoIP', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.2', 11 | 'description': 'Leverages the ipinfodb.com API to geolocate a host by IP address. Updates the \'hosts\' table ' 12 | 'with the results.', 13 | 'required_keys': ['ipinfodb_api'], 14 | 'query': 'SELECT DISTINCT ip_address FROM hosts WHERE ip_address IS NOT NULL', 15 | 'options': ( 16 | ('rate_limit', 0.8, False, 'allows 1 request per the specified seconds'), 17 | ), 18 | 'comments': ( 19 | 'Free API access requires the use of rate limiting.', 20 | 'If you are getting temporarily denied, increase rate as needed.', 21 | 'Unset rate_limit or set to 0 for no limit.' 22 | ), 23 | } 24 | 25 | def module_run(self, hosts): 26 | api_key = self.keys.get('ipinfodb_api') 27 | for host in hosts: 28 | url = (f"http://api.ipinfodb.com/v3/ip-city/?key={api_key}&ip={host}&format=json") 29 | resp = self.request('GET', url) 30 | try: 31 | jsonobj = resp.json() 32 | except ValueError: 33 | self.error(f"Invalid JSON response for '{host}'.\n{resp.text}") 34 | continue 35 | if jsonobj['statusCode'].lower() == 'error': 36 | self.error(jsonobj['statusMessage']) 37 | continue 38 | 39 | # Used to catch the garbage data and null it out so it does not clog up the database. 40 | for x in ['cityName', 'regionName', 'countryName', 'latitude', 'longitude']: 41 | if jsonobj[x] == '-' or jsonobj[x] == '0': 42 | jsonobj[x] = None 43 | 44 | region = ', '.join([jsonobj[x] for x in ['cityName', 'regionName'] if jsonobj[x]]) or None 45 | country = jsonobj['countryName'] 46 | latitude = jsonobj['latitude'] 47 | longitude = jsonobj['longitude'] 48 | self.output(f"{host} - {latitude},{longitude} - {', '.join([x for x in [region, country] if x])}") 49 | self.query('UPDATE hosts SET region=?, country=?, latitude=?, longitude=? WHERE ip_address=?', 50 | (region, country, latitude, longitude, host)) 51 | if self.options['rate_limit']: 52 | time.sleep(self.options['rate_limit']) 53 | -------------------------------------------------------------------------------- /modules/recon/netblocks-ports/censysio.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import time 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Censys.io Netblock Enumerator', 8 | 'author': 'John Askew (https://bitbucket.org/skew)', 9 | 'version': '1.0', 10 | 'description': 'Queries the censys.io API to enumerate information about netblocks.', 11 | 'required_keys': ['censysio_id', 'censysio_secret'], 12 | 'comments': ( 13 | 'To enumerate ports for hosts, use the following query as the SOURCE option.', 14 | '\tSELECT DISTINCT ip_address || \'/32\' FROM hosts WHERE ip_address IS NOT NULL', 15 | 'Leak rates may vary. Each user\'s leak rate is listed in their Censys.io account.', 16 | ), 17 | 'query': 'SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT NULL', 18 | 'options': ( 19 | ('rate', .2, True, 'search endpoint leak rate (tokens/second)'), 20 | ('limit', True, True, 'toggle rate limiting'), 21 | ), 22 | } 23 | 24 | def module_run(self, netblocks): 25 | for netblock in netblocks: 26 | self.heading(netblock, level=0) 27 | page = 1 28 | while True: 29 | resp = self._get_page(netblock, page) 30 | if resp.status_code != 200: 31 | self.error(f"Error: '{resp.json().get('error')}'") 32 | break 33 | self._load_results(resp) 34 | if resp.json().get('metadata').get('page') >= resp.json().get('metadata').get('pages'): 35 | break 36 | self.verbose('Fetching the next page of results...') 37 | page += 1 38 | 39 | def _get_page(self, netblock, page): 40 | payload = { 41 | 'query': f"ip:{netblock}", 42 | 'page': page, 43 | 'fields': ['ip', 'protocols'] 44 | } 45 | credentials = (self.keys.get('censysio_id'), self.keys.get('censysio_secret')) 46 | resp = self.request('POST', 'https://censys.io/api/v1/search/ipv4', json=payload, auth=credentials) 47 | if self.options['limit']: 48 | time.sleep(1 / self.options['rate']) 49 | return resp 50 | 51 | def _load_results(self, resp): 52 | for result in resp.json().get('results'): 53 | ip_address = result.get('ip') 54 | for service in result.get('protocols'): 55 | port, protocol = service.split('/') 56 | self.insert_ports(ip_address=ip_address, port=port, protocol=protocol) 57 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/bing_domain_api.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.search import BingAPIMixin 3 | from recon.utils.parsers import parse_hostname 4 | import re 5 | 6 | class Module(BaseModule, BingAPIMixin): 7 | 8 | meta = { 9 | 'name': 'Bing API Hostname Enumerator', 10 | 'author': 'Marcus Watson (@BranMacMuffin)', 11 | 'version': '1.0', 12 | 'description': 'Leverages the Bing API and "domain:" advanced search operator to harvest hosts. Updates the \'hosts\' table with the results.', 13 | 'required_keys': ['bing_api'], 14 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 15 | 'options': ( 16 | ('limit', 0, True, 'limit total number of api requests (0 = unlimited)'), 17 | ), 18 | } 19 | 20 | def module_run(self, domains): 21 | limit = self.options['limit'] 22 | requests = 0 23 | for domain in domains: 24 | self.heading(domain, level=0) 25 | hosts = [] 26 | results = [] 27 | pages = 1 28 | base_query = f"domain:{domain}" 29 | while not limit or requests < limit: 30 | query = base_query 31 | # build query string based on api limitations 32 | for host in hosts: 33 | omit_domain = f" -domain:{host}" 34 | # https://msdn.microsoft.com/en-us/library/dn760794.aspx 35 | if len(query) + len(omit_domain) >= 1500: 36 | break 37 | query += omit_domain 38 | # make api requests 39 | if limit and requests + pages > limit: 40 | pages = limit - requests 41 | last_len = len(results) 42 | results = self.search_bing_api(query, pages) 43 | requests += pages 44 | # iterate through results and add new hosts 45 | flag = False 46 | for result in results: 47 | host = parse_hostname(result['displayUrl']) 48 | if host.endswith('.'+domain) and host not in hosts: 49 | hosts.append(host) 50 | self.insert_hosts(host) 51 | flag = True 52 | if not flag and last_len == len(results): 53 | break 54 | elif not flag and last_len != len(results): 55 | pages += 1 56 | self.verbose(f"No new hosts found for the current query. Increasing depth to '{pages}' pages.") 57 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/google_site_web.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.search import GoogleWebMixin 3 | import re 4 | 5 | class Module(BaseModule, GoogleWebMixin): 6 | 7 | meta = { 8 | 'name': 'Google Hostname Enumerator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Harvests hosts from Google.com by using the \'site\' search operator. Updates the \'hosts\' table with the results.', 12 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 13 | } 14 | 15 | def module_run(self, domains): 16 | for domain in domains: 17 | self.heading(domain, level=0) 18 | base_query = 'site:' + domain 19 | regmatch = re.compile(rf"//([^/]*\.{domain})") 20 | hosts = [] 21 | # control variables 22 | new = True 23 | page = 1 24 | nr = 100 25 | # execute search engine queries and scrape results storing subdomains in a list 26 | # loop until no new subdomains are found 27 | while new: 28 | # build query based on results of previous results 29 | query = '' 30 | for host in hosts: 31 | query += f" -site:{host}" 32 | # send query to search engine 33 | results = self.search_google_web(base_query + query, limit=1, start_page=page) 34 | # extract hosts from search results 35 | sites = [] 36 | for link in results: 37 | site = regmatch.search(link) 38 | if site is not None: 39 | sites.append(site.group(1)) 40 | # create a unique list 41 | sites = list(set(sites)) 42 | # add subdomain to list if not already exists 43 | new = False 44 | for site in sites: 45 | if site not in hosts: 46 | hosts.append(site) 47 | new = True 48 | self.insert_hosts(site) 49 | if not new: 50 | # exit if all subdomains have been found 51 | if not results: 52 | break 53 | else: 54 | # intelligently paginate separate from the framework to optimize the number of queries required 55 | page += 1 56 | self.verbose(f"No New Subdomains Found on the Current Page. Jumping to Result {(page*nr)+1}.") 57 | new = True 58 | -------------------------------------------------------------------------------- /modules/recon/domains-domains/brute_suffix.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.resolver import ResolverMixin 3 | import dns.resolver 4 | import os 5 | 6 | class Module(BaseModule, ResolverMixin): 7 | 8 | meta = { 9 | 'name': 'DNS Public Suffix Brute Forcer', 10 | 'author': 'Marcus Watson (@BranMacMuffin)', 11 | 'version': '1.1', 12 | 'description': 'Brute forces TLDs and SLDs using DNS. Updates the \'domains\' table with the results.', 13 | 'comments': ( 14 | 'TLDs: https://data.iana.org/TLD/tlds-alpha-by-domain.txt', 15 | 'SLDs: https://raw.github.com/gavingmiller/second-level-domains/master/SLDs.csv', 16 | ), 17 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 18 | 'options': ( 19 | ('suffixes', os.path.join(BaseModule.data_path, 'suffixes.txt'), True, 'path to public suffix wordlist'), 20 | ), 21 | 'files': ['suffixes.txt'], 22 | } 23 | 24 | def module_run(self, domains): 25 | max_attempts = 3 26 | resolver = self.get_resolver() 27 | with open(self.options['suffixes']) as fp: 28 | words = [line.strip().lower() for line in fp if len(line)>0 and line[0] != '#'] 29 | for domain in domains: 30 | self.heading(domain, level=0) 31 | domain_root = domain.split('.')[0] 32 | for word in words: 33 | attempt = 0 34 | while attempt < max_attempts: 35 | domain = f"{domain_root}.{word}" 36 | try: 37 | answers = resolver.query(domain, 'SOA') 38 | except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): 39 | self.verbose(f"{domain} => No record found.") 40 | except dns.resolver.Timeout: 41 | self.verbose(f"{domain} => Request timed out.") 42 | attempt += 1 43 | continue 44 | else: 45 | # process answers 46 | for answer in answers.response.answer: 47 | if answer.rdtype == 6: 48 | soa = answer.name.to_text()[:-1] 49 | self.alert(f"{domain} => (SOA) {soa}") 50 | # use "host" rather than "soa" as sometimes the SOA record has a CNAME 51 | self.insert_domains(domain) 52 | # break out of the loop 53 | attempt = max_attempts 54 | -------------------------------------------------------------------------------- /modules/recon/contacts-contacts/mangle.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import re 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Contact Name Mangler', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.0', 10 | 'description': 'Applies a mangle pattern to all of the contacts stored in the database, creating email addresses or usernames for each harvested contact. Updates the \'contacts\' table with the results.', 11 | 'comments': ( 12 | 'Pattern options: ,,,,
  • ,', 13 | 'Example: . => j.doe@domain.com', 14 | 'Note: Omit the \'domain\' option to create usernames', 15 | ), 16 | 'query': 'SELECT rowid, first_name, middle_name, last_name, email FROM contacts ORDER BY first_name', 17 | 'options': ( 18 | ('domain', None, False, 'target email domain'), 19 | ('pattern', '.', True, 'pattern applied to mangle first and last name'), 20 | ('substitute', '-', True, 'character to substitute for invalid email address characters'), 21 | ('max-length', 30, True, 'maximum length of email address prefix or username'), 22 | ('overwrite', False, True, 'overwrite existing email addresses'), 23 | ), 24 | } 25 | 26 | def module_run(self, contacts): 27 | for contact in contacts: 28 | if not self.options['overwrite'] and contact[4] is not None: 29 | continue 30 | row = contact[0] 31 | fname = contact[1] 32 | mname = contact[2] 33 | lname = contact[3] 34 | email = self.options['pattern'] 35 | sub_pattern = '[\s]' 36 | substitute = self.options['substitute'] 37 | items = {'': '', '': '', '': '', '': '', '': '', '
  • ': ''} 38 | if fname: 39 | items[''] = re.sub(sub_pattern, substitute, fname.lower()) 40 | items[''] = fname[:1].lower() 41 | if mname: 42 | items[''] = re.sub(sub_pattern, substitute, mname.lower()) 43 | items[''] = mname[:1].lower() 44 | if lname: 45 | items[''] = re.sub(sub_pattern, substitute, lname.lower()) 46 | items['
  • '] = lname[:1].lower() 47 | for item in items: 48 | email = email.replace(item, items[item]) 49 | email = email[:self.options['max-length']] 50 | domain = self.options['domain'] 51 | if domain:email = f"{email}@{domain}" 52 | self.output(f"{fname} {lname} => {email}") 53 | self.query('UPDATE contacts SET email=? WHERE rowid=?', (email, row)) 54 | -------------------------------------------------------------------------------- /modules/recon/companies-multi/censys_tls_subjects.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Domains by Company", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves the TLS certificates for a domain. This module queries" 14 | " the 'services.tls.certificates.leaf_data.subject.organization'" 15 | " field and updates the 'hosts' and 'ports' tables with the" 16 | " results." 17 | ), 18 | "query": ( 19 | "SELECT DISTINCT company FROM companies WHERE company IS NOT NULL" 20 | ), 21 | "required_keys": ["censysio_id", "censysio_secret"], 22 | "dependencies": ["censys>=2.1.2"], 23 | } 24 | 25 | def module_run(self, companies): 26 | api_id = self.get_key("censysio_id") 27 | api_secret = self.get_key("censysio_secret") 28 | c = CensysHosts(api_id, api_secret) 29 | for company in companies: 30 | company = company.strip('"') 31 | self.heading(company, level=0) 32 | try: 33 | query = c.search( 34 | f'services.tls.certificates.leaf_data.subject.organization:"{company}"', 35 | virtual_hosts="INCLUDE", 36 | ) 37 | except CensysException: 38 | self.print_exception() 39 | continue 40 | for hit in query(): 41 | ip = hit["ip"] 42 | name = hit.get("name") 43 | if name: 44 | self.insert_domains( 45 | domain=name, notes="+".join((ip, name)) 46 | ) 47 | common_kwargs = { 48 | "ip_address": ip, 49 | "host": name, 50 | } 51 | location = hit.get("location", {}) 52 | coords = location.get("coordinates", {}) 53 | self.insert_hosts( 54 | region=location.get("continent"), 55 | country=location.get("country"), 56 | latitude=coords.get("latitude"), 57 | longitude=coords.get("longitude"), 58 | **common_kwargs, 59 | ) 60 | for service in hit.get("services", []): 61 | self.insert_ports( 62 | port=service["port"], 63 | protocol=service["transport_protocol"], 64 | notes=service["service_name"], 65 | **common_kwargs, 66 | ) 67 | -------------------------------------------------------------------------------- /data/template_html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recon-ng Reconnaissance Report 5 | 6 | 28 | 97 | 98 | 99 |
    100 |
    %s
    101 |
    Recon-ng Reconnaissance Report
    102 | 103 |
    104 |
    105 | 106 | %s 107 | 108 |
    109 |
    110 | 111 |
    112 | 113 | -------------------------------------------------------------------------------- /modules/recon/profiles-contacts/bing_linkedin_contacts.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.search import BingAPIMixin 3 | from recon.utils.parsers import parse_name 4 | import re 5 | 6 | 7 | class Module(BaseModule, BingAPIMixin): 8 | 9 | meta = { 10 | "name": "Bing LinkedIn Profile Contact Harvester", 11 | "author": "Cam Barts (@cam-barts)", 12 | "version": "1.2", 13 | "description": "Harvests Basic Contact Information from Bing based on LinkedIn profiles.", 14 | "required_keys": ["bing_api"], 15 | "comments": ( 16 | "Use Bing's top search results for LinkedIn urls to gather names, titles, and companies", 17 | "This works for profiles that are set to public on LinkedIn", 18 | ), 19 | "query": "SELECT DISTINCT url FROM profiles WHERE resource='LinkedIn'", 20 | "options": (), 21 | } 22 | 23 | def module_run(self, urls): 24 | for url in urls: 25 | self.get_contact_info(url) 26 | 27 | def get_contact_info(self, url): 28 | search_result = self.search_bing_api(url, 1) 29 | 30 | # Search by url. If the url doesn't match, it has potential to be a different person 31 | if search_result and search_result[0]["url"] == url: 32 | search_result = search_result[0] 33 | # "Name" is a misnomer, it actually refers to the link title 34 | link_title = search_result["name"] 35 | 36 | # Split the title on the pipe to get rid of "linkedIn" portion at the end 37 | name_and_title = link_title.split("|")[0] 38 | # Split whats left on the Dashes, which is usually name - title - company 39 | # some european LinkedIn sites use em-dash 40 | EM_DASH = b'\xe2\x80\x93'.decode('utf-8') 41 | delimeter_expression = '- | ' + EM_DASH 42 | name_title_company_list = re.split(delimeter_expression, name_and_title) 43 | # Parse out name 44 | fullname = name_title_company_list[0] 45 | fname, mname, lname = parse_name(fullname) 46 | 47 | # Sometimes "LinkedIn" is left at the end anyway, and we don't want to confuse that for the company 48 | if "linkedin" not in name_title_company_list[-1].lower(): 49 | company = name_title_company_list[-1] 50 | else: 51 | company = False 52 | 53 | # Try to parse out a title and company if it's there 54 | if "linkedin" not in name_title_company_list[1].lower(): 55 | if not company: 56 | title = name_title_company_list[1] 57 | else: 58 | title = f"{name_title_company_list[1]} at {company}" 59 | self.insert_contacts( 60 | first_name=fname, middle_name=mname, last_name=lname, title=title 61 | ) 62 | else: 63 | self.insert_contacts( 64 | first_name=fname, middle_name=mname, last_name=lname 65 | ) 66 | -------------------------------------------------------------------------------- /modules/recon/domains-contacts/hunter_io.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.core.framework import FrameworkException 3 | 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | "name": "Hunter.io Email Address Harvester", 9 | "author": "Super Choque (@aplneto)", 10 | "version": "1.3", 11 | "description": "Uses Hunter.io to find email addresses for given domains.", 12 | "dependencies": [], 13 | "files": [], 14 | "required_keys": ['hunter_io'], 15 | "query": "SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL", 16 | 'options': ( 17 | ('count', 10, True, 'Limit the amount of results returned. (10 = Free Account)'), 18 | ) 19 | } 20 | 21 | def module_run(self, domains): 22 | self.__key = self.keys['hunter_io'] 23 | 24 | if self.__key is None: 25 | self.alert("No API key detected, using trial mode instead") 26 | self.__uri = "https://api.hunter.io/trial/v2/domain-search" 27 | else: 28 | self.__uri = "https://api.hunter.io/v2/domain-search" 29 | 30 | for domain in domains: 31 | self.__search_domain(domain) 32 | 33 | def __search_domain(self, domain): 34 | self.output( 35 | "domain: {}".format(domain) 36 | ) 37 | 38 | offset = 0 39 | results = 0 40 | first_query = True 41 | 42 | while (offset < results) or first_query: 43 | baseparams = { 44 | "domain": domain, 45 | "api_key": self.__key, 46 | "limit": self.options['count'], 47 | "offset": offset 48 | } 49 | 50 | response = self.request( 51 | "GET", self.__uri, params=baseparams 52 | ) 53 | 54 | information = response.json() 55 | 56 | if response.status_code != 200: 57 | self.error( 58 | "Something went wrong!\n" + 59 | "status code {} for domain \"{}\"".format( 60 | response.status_code, domain 61 | ) 62 | ) 63 | self.debug(information) 64 | return 65 | else: 66 | results = information['meta']['results'] 67 | 68 | self.process_data(information['data']) 69 | 70 | offset += 100 71 | 72 | if first_query: 73 | first_query = False 74 | 75 | self.verbose("{} people found for {}".format(results, domain)) 76 | 77 | def process_data(self, data): 78 | country = data['country'] 79 | region = data['state'] 80 | 81 | for registry in data['emails']: 82 | contact = { 83 | "first_name": registry["first_name"], 84 | "last_name": registry["last_name"], 85 | "email": registry["value"], 86 | "country": country, 87 | "region": region 88 | } 89 | self.insert_contacts(**contact) 90 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/netcraft.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from http.cookiejar import CookieJar 3 | from urllib.parse import unquote_plus, urlencode 4 | import re 5 | import hashlib 6 | import time 7 | import random 8 | 9 | class Module(BaseModule): 10 | 11 | meta = { 12 | 'name': 'Netcraft Hostname Enumerator', 13 | 'author': 'thrapt (thrapt@gmail.com)', 14 | 'version': '1.1', 15 | 'description': 'Harvests hosts from Netcraft.com. Updates the \'hosts\' table with the results.', 16 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 17 | } 18 | 19 | def module_run(self, domains): 20 | url = 'http://searchdns.netcraft.com/' 21 | pattern = r' {plaintext}") 51 | self.query(f"UPDATE credentials SET password='{plaintext}', type='{hashtype}' WHERE hash='{hashstr}'") 52 | else: 53 | self.verbose(f"Value not found for hash: {hashstr}") 54 | # sleep to avoid lock-out 55 | time.sleep(random.randint(3,5)) 56 | 57 | 58 | def crack(hashstr, wordlist, hashlist): 59 | for word in wordlist: 60 | for hashfunc in hashlist: 61 | try: 62 | if hashfunc(word.encode('utf-8')).hexdigest().lower() == hashstr.lower(): 63 | return word, hashtype 64 | except TypeError: 65 | continue 66 | return None, None 67 | -------------------------------------------------------------------------------- /modules/recon/netblocks-hosts/censys_netblock.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Hosts by Netblock", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves hosts and ports for a neyblock. " 14 | "Updates the 'hosts' and the 'ports' tables with the results." 15 | ), 16 | "query": ( 17 | "SELECT DISTINCT netblock FROM netblocks WHERE netblock IS NOT" 18 | " NULL" 19 | ), 20 | "required_keys": ["censysio_id", "censysio_secret"], 21 | "options": [ 22 | ( 23 | "virtual_hosts", 24 | "EXCLUDE", 25 | False, 26 | "Whether to include virtual hosts in the results", 27 | ), 28 | ( 29 | "per_page", 30 | "100", 31 | False, 32 | "The number of results to return per page", 33 | ), 34 | ( 35 | "pages", 36 | "1", 37 | False, 38 | "The number of pages to retrieve", 39 | ), 40 | ], 41 | "dependencies": ["censys>=2.1.2"], 42 | } 43 | 44 | def module_run(self, netblocks): 45 | api_id = self.get_key("censysio_id") 46 | api_secret = self.get_key("censysio_secret") 47 | c = CensysHosts(api_id, api_secret) 48 | for netblock in netblocks: 49 | self.heading(netblock, level=0) 50 | try: 51 | query = c.search( 52 | f"ip:{netblock}", 53 | per_page=int(self.options.get("PER_PAGE", "100")), 54 | pages=int(self.options.get("PAGES", "1")), 55 | virtual_hosts=self.options.get("VIRTUAL_HOSTS", "EXCLUDE"), 56 | ) 57 | except CensysException: 58 | self.print_exception() 59 | continue 60 | for hit in query(): 61 | common_kwargs = { 62 | "ip_address": hit["ip"], 63 | "host": hit.get("name"), 64 | } 65 | location = hit.get("location", {}) 66 | coords = location.get("coordinates", {}) 67 | self.insert_hosts( 68 | region=location.get("continent"), 69 | country=location.get("country"), 70 | latitude=coords.get("latitude"), 71 | longitude=coords.get("longitude"), 72 | **common_kwargs, 73 | ) 74 | for service in hit.get("services", []): 75 | self.insert_ports( 76 | port=service["port"], 77 | protocol=service["transport_protocol"], 78 | notes=service["service_name"], 79 | **common_kwargs, 80 | ) 81 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/censys_domain.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Hosts by domain", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves hosts for a domain. This module queries queries domain" 14 | " names and updates the 'hosts' and the 'ports' tables with the" 15 | " results." 16 | ), 17 | "query": ( 18 | "SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL" 19 | ), 20 | "required_keys": ["censysio_id", "censysio_secret"], 21 | "options": [ 22 | ( 23 | "virtual_hosts", 24 | "ONLY", 25 | False, 26 | "Whether to include virtual hosts in the results", 27 | ), 28 | ( 29 | "per_page", 30 | "100", 31 | False, 32 | "The number of results to return per page", 33 | ), 34 | ( 35 | "pages", 36 | "1", 37 | False, 38 | "The number of pages to retrieve", 39 | ), 40 | ], 41 | "dependencies": ["censys>=2.1.2"], 42 | } 43 | 44 | def module_run(self, domains): 45 | api_id = self.get_key("censysio_id") 46 | api_secret = self.get_key("censysio_secret") 47 | c = CensysHosts(api_id, api_secret) 48 | for domain in domains: 49 | domain = domain.strip('"') 50 | self.heading(domain, level=0) 51 | try: 52 | query = c.search( 53 | f"{domain}", 54 | per_page=int(self.options.get("PER_PAGE", "100")), 55 | pages=int(self.options.get("PAGES", "1")), 56 | virtual_hosts=self.options.get("VIRTUAL_HOSTS", "ONLY"), 57 | ) 58 | except CensysException: 59 | self.print_exception() 60 | continue 61 | for hit in query(): 62 | common_kwargs = { 63 | "ip_address": hit["ip"], 64 | "host": hit.get("name"), 65 | } 66 | location = hit.get("location", {}) 67 | coords = location.get("coordinates", {}) 68 | self.insert_hosts( 69 | region=location.get("continent"), 70 | country=location.get("country"), 71 | latitude=coords.get("latitude"), 72 | longitude=coords.get("longitude"), 73 | **common_kwargs, 74 | ) 75 | for service in hit.get("services", []): 76 | self.insert_ports( 77 | port=service["port"], 78 | protocol=service["transport_protocol"], 79 | notes=service["service_name"], 80 | **common_kwargs, 81 | ) 82 | -------------------------------------------------------------------------------- /modules/recon/domains-contacts/wikileaker.py: -------------------------------------------------------------------------------- 1 | # module specific imports 2 | import re 3 | import time 4 | 5 | import lxml.html 6 | from recon.core.module import BaseModule 7 | 8 | 9 | class Module(BaseModule): 10 | 11 | meta = { 12 | 'name': 'WikiLeaker', 13 | 'author': 'Joe Gray (@C_3PJoe)', 14 | 'version': '1.0', 15 | 'description': 'A WikiLeaks scraper inspired by the Datasploit module previously written in Python2. It ' 16 | 'searches Wikileaks for leaks containing the subject domain. If anything is found, this module ' 17 | 'will seek to parse out the URL, Sender Email, Date, Leak, and Subject of the email. This will ' 18 | 'update the \'Contacts\' table with the results.', 19 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 20 | } 21 | 22 | def module_run(self, domains): 23 | for domain in domains: 24 | page_count = 1 25 | while True: 26 | URL = 'https://search.wikileaks.org/?query=&exact_phrase=' + domain + \ 27 | '&include_external_sources=True&order_by=newest_document_date&page=' + str(page_count) 28 | self.verbose(URL) 29 | resp = self.request('GET', URL) 30 | time.sleep(1) 31 | if resp.status_code != 200: 32 | self.alert('An error occurred: ' + str(resp.status_code)) 33 | break 34 | else: 35 | root = lxml.html.fromstring(resp.text) 36 | search_data = root.xpath('//div[@class="result"]') 37 | 38 | if len(search_data) > 0: 39 | for i in search_data: 40 | link = i.xpath("concat(div/h4/a[contains(@href, '/emails/emailid/')]/@href, '')").strip() 41 | 42 | if link: 43 | subject = i.xpath("concat(div/h4/a, '')").strip() 44 | leak = i.xpath("concat(div/div[@class='leak-label'], '')").strip() 45 | created = i.xpath("concat(div/div[@class='dates']/div[@class='date' and " 46 | "contains(text(), 'Created')]/span, '')").strip() 47 | excerpt = i.xpath("concat(div[@class='info']/div[@class='excerpt'], '')").strip() 48 | 49 | emails = re.findall("email:\\xa0([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)", 50 | excerpt) 51 | 52 | for email in emails: 53 | self.alert(f'Leak: {leak}') 54 | self.output(f'URL: {link}') 55 | self.verbose(f'Date: {created}') 56 | self.verbose(f'Sender: {email.strip()}') 57 | self.verbose(f'Subject: {subject}') 58 | self.insert_contacts(email=email.strip(), notes=link) 59 | else: 60 | break 61 | 62 | page_count += 1 63 | -------------------------------------------------------------------------------- /modules/recon/hosts-hosts/censys_query.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Hosts by Search Query", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves details for hosts matching an arbitrary Censys query." 14 | " Updates the 'hosts', 'domains', and 'ports' tables with the" 15 | " results." 16 | ), 17 | "required_keys": ["censysio_id", "censysio_secret"], 18 | "options": [ 19 | ( 20 | "censys_query", 21 | 'services.http.response.html_title: "Welcome to recon-ng"', 22 | True, 23 | "The Censys Search Query to execute", 24 | ), 25 | ( 26 | "virtual_hosts", 27 | "EXCLUDE", 28 | False, 29 | "Whether to include virtual hosts in the results", 30 | ), 31 | ( 32 | "per_page", 33 | "100", 34 | False, 35 | "The number of results to return per page", 36 | ), 37 | ( 38 | "pages", 39 | "1", 40 | False, 41 | "The number of pages to retrieve", 42 | ), 43 | ], 44 | "dependencies": ["censys>=2.1.2"], 45 | } 46 | 47 | def module_run(self): 48 | api_id = self.get_key("censysio_id") 49 | api_secret = self.get_key("censysio_secret") 50 | c = CensysHosts(api_id, api_secret) 51 | try: 52 | query = c.search( 53 | self.options["CENSYS_QUERY"], 54 | per_page=int(self.options.get("PER_PAGE", "100")), 55 | pages=int(self.options.get("PAGES", "1")), 56 | virtual_hosts=self.options.get("VIRTUAL_HOSTS", "EXCLUDE"), 57 | ) 58 | except CensysException: 59 | self.print_exception() 60 | return 61 | for hit in query(): 62 | ip = hit["ip"] 63 | name = hit.get("name") 64 | if name: 65 | self.insert_domains(domain=name, notes="+".join((ip, name))) 66 | common_kwargs = { 67 | "ip_address": ip, 68 | "host": name, 69 | } 70 | location = hit.get("location", {}) 71 | coords = location.get("coordinates", {}) 72 | self.insert_hosts( 73 | region=location.get("continent"), 74 | country=location.get("country"), 75 | latitude=coords.get("latitude"), 76 | longitude=coords.get("longitude"), 77 | **common_kwargs, 78 | ) 79 | for service in hit.get("services", []): 80 | self.insert_ports( 81 | port=service["port"], 82 | protocol=service["transport_protocol"], 83 | notes=service["service_name"], 84 | **common_kwargs, 85 | ) 86 | -------------------------------------------------------------------------------- /modules/recon/locations-pushpins/youtube.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from datetime import datetime 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'YouTube Geolocation Search', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.2', 10 | 'description': 'Searches the YouTube API for media in the specified proximity to a location.', 11 | 'required_keys': ['google_api'], 12 | 'query': 'SELECT DISTINCT latitude || \',\' || longitude FROM locations WHERE latitude IS NOT NULL AND longitude IS NOT NULL', 13 | 'options': ( 14 | ('radius', 1, True, 'radius in kilometers'), 15 | ), 16 | } 17 | 18 | def module_run(self, locations): 19 | self.api_key = self.keys.get('google_api') 20 | self.url = 'https://www.googleapis.com/youtube/v3/%s' 21 | payload = {'part': 'snippet', 'type': 'video', 'key': self.api_key, 'locationRadius': f"{self.options['radius']}km", 'maxResults': 50} 22 | for location in locations: 23 | self.heading(location, level=0) 24 | payload['location'] = location 25 | processed = 0 26 | while True: 27 | resp = self.request('GET', self.url % 'search', params=payload) 28 | if 'error' in resp.json(): 29 | self.alert(resp.json()['error']['message']) 30 | break 31 | if not processed: 32 | self.output(f"Collecting data for {resp.json()['pageInfo']['totalResults']} videos...") 33 | if not 'items' in resp.json(): 34 | break 35 | for video in resp.json()['items']: 36 | source = 'YouTube' 37 | screen_name = video['snippet']['channelTitle'] or 'Unknown' 38 | profile_name = screen_name 39 | profile_url = f"http://www.youtube.com/channel/{video['snippet']['channelId']}" 40 | media_url = f"https://www.youtube.com/watch?v={video['id']['videoId']}" 41 | thumb_url = video['snippet']['thumbnails']['high']['url'] 42 | message = video['snippet']['title'] 43 | latitude, longitude = self.get_video_geo(video['id']['videoId']) 44 | time = datetime.strptime(video['snippet']['publishedAt'], '%Y-%m-%dT%H:%M:%S%fZ') 45 | self.insert_pushpins(source, screen_name, profile_name, profile_url, media_url, thumb_url, message, latitude, longitude, time) 46 | processed += len(resp.json()['items']) 47 | self.verbose(f"{processed} videos processed.") 48 | if 'nextPageToken' in resp.json(): 49 | payload['pageToken'] = resp.json()['nextPageToken'] 50 | continue 51 | break 52 | 53 | def get_video_geo(self, vid): 54 | payload = {'part': 'recordingDetails', 'id': vid, 'key': self.api_key} 55 | resp = self.request('GET', self.url % 'videos', params=payload) 56 | latitude = resp.json()['items'][0]['recordingDetails']['location']['latitude'] 57 | longitude = resp.json()['items'][0]['recordingDetails']['location']['longitude'] 58 | return latitude, longitude 59 | -------------------------------------------------------------------------------- /modules/recon/locations-pushpins/shodan.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | import shodan 3 | import time 4 | from datetime import datetime 5 | 6 | 7 | def prep_host(host_data, hostname): 8 | os = host_data['os'] 9 | hostname = hostname 10 | host_port = f"{host_data['ip_str']}:{host_data['port']}" 11 | source = 'Shodan' 12 | screen_name = host_port 13 | profile_name = host_port 14 | profile_url = f"http://{host_port}" 15 | media_url = f"https://www.shodan.io/host/{host_data['ip_str']}" 16 | thumb_url = 'https://gravatar.com/avatar/ffc4048d63729d4932fd3cc45139174f?s=300' 17 | message = ( 18 | f"Hostname: {hostname} | City: {host_data['location']['city']} | State: {host_data['location']['region_code']} " 19 | f"| Country: {host_data['location']['country_name']} | OS: {os}") 20 | latitude = host_data['location']['latitude'] 21 | longitude = host_data['location']['longitude'] 22 | time = datetime.strptime(host_data['timestamp'], '%Y-%m-%dT%H:%M:%S.%f') 23 | return source, screen_name, profile_name, profile_url, media_url, thumb_url, message, latitude, longitude, time 24 | 25 | 26 | class Module(BaseModule): 27 | 28 | meta = { 29 | 'name': 'Shodan Geolocation Search', 30 | 'author': 'Tim Tomes (@lanmaster53) & Ryan Hays (@_ryanhays)', 31 | 'version': '1.1', 32 | 'description': 'Searches Shodan for media in the specified proximity to a location.', 33 | 'required_keys': ['shodan_api'], 34 | 'comments': ( 35 | 'Shodan \'geo\' searches can take a long time to complete. If receiving timeout errors, increase the global' 36 | ' TIMEOUT option.', 37 | ), 38 | 'query': 'SELECT DISTINCT latitude || \',\' || longitude FROM locations WHERE latitude IS NOT NULL AND ' 39 | 'longitude IS NOT NULL', 40 | 'options': ( 41 | ('radius', 1, True, 'radius in kilometers'), 42 | ('limit', 1, True, 'limit number of api requests per input source (0 = unlimited)'), 43 | ), 44 | 'dependencies': ['shodan'] 45 | } 46 | 47 | def module_run(self, points): 48 | limit = self.options['limit'] 49 | rad = self.options['radius'] 50 | api = shodan.Shodan(self.keys.get('shodan_api')) 51 | 52 | for point in points: 53 | self.heading(point, level=0) 54 | query = f"geo:{point},{rad}" 55 | 56 | try: 57 | page = 1 58 | rec_count = 0 59 | total_results = 1 60 | while rec_count < total_results: 61 | results = api.search(query, page=page) 62 | total_results = results['total'] 63 | 64 | for host in results['matches']: 65 | rec_count += 1 66 | if len(host['hostnames']) > 0: 67 | for hostname in host['hostnames']: 68 | self.insert_pushpins(*prep_host(host, hostname)) 69 | else: 70 | self.insert_pushpins(*prep_host(host, 'None')) 71 | 72 | page += 1 73 | time.sleep(limit) 74 | 75 | except shodan.exception.APIError: 76 | pass 77 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/bing_domain_web.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from http.cookiejar import CookieJar 3 | from urllib.parse import quote_plus 4 | import re 5 | import time 6 | import random 7 | 8 | class Module(BaseModule): 9 | 10 | meta = { 11 | 'name': 'Bing Hostname Enumerator', 12 | 'author': 'Tim Tomes (@lanmaster53)', 13 | 'version': '1.1', 14 | 'description': 'Harvests hosts from Bing.com by using the \'site\' search operator. Updates the \'hosts\' table with the results.', 15 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 16 | } 17 | 18 | def module_run(self, domains): 19 | base_url = 'https://www.bing.com/search' 20 | for domain in domains: 21 | self.heading(domain, level=0) 22 | base_query = 'domain:' + domain 23 | pattern = f'"b_algo">

    2059 characters not including the protocol 42 | if len(url) > 2066: url = url[:2066] 43 | self.verbose(f"URL: {url}") 44 | # send query to search engine 45 | resp = self.request('GET', url, cookies=cookiejar) 46 | if resp.status_code != 200: 47 | self.alert('Bing has encountered an error. Please submit an issue for debugging.') 48 | break 49 | content = resp.text 50 | sites = re.findall(pattern, content) 51 | # create a unique list 52 | sites = list(set(sites)) 53 | new = False 54 | # add subdomain to list if not already exists 55 | for site in sites: 56 | if site not in subs: 57 | subs.append(site) 58 | new = True 59 | host = f"{site}.{domain}" 60 | self.insert_hosts(host) 61 | if not new: 62 | # exit if all subdomains have been found 63 | if not '>Next' in content: 64 | break 65 | else: 66 | page += 1 67 | self.verbose(f"No New Subdomains Found on the Current Page. Jumping to Result {(page*nr)+1}.") 68 | new = True 69 | # sleep script to avoid lock-out 70 | self.verbose('Sleeping to avoid lockout...') 71 | time.sleep(random.randint(5,15)) 72 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/builtwith.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.utils.parsers import parse_name 3 | import textwrap 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'BuiltWith Enumerator', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.1', 11 | 'description': 'Leverages the BuiltWith API to identify hosts, technologies, and contacts associated with a domain.', 12 | 'required_keys': ['builtwith_api'], 13 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 14 | 'options': ( 15 | ('show_all', True, True, 'display technologies'), 16 | ), 17 | } 18 | 19 | def module_run(self, domains): 20 | key = self.keys.get('builtwith_api') 21 | url = 'https://api.builtwith.com/v6/api.json' 22 | title = 'BuiltWith contact' 23 | for domain in domains: 24 | self.heading(domain, level=0) 25 | payload = {'key': key, 'lookup': domain} 26 | resp = self.request('GET', url, params=payload) 27 | if 'error' in resp.json(): 28 | self.error(resp.json()['error']) 29 | continue 30 | for result in resp.json()['Results']: 31 | # extract and add emails to contacts 32 | emails = result['Meta']['Emails'] 33 | if emails is None: emails = [] 34 | for email in emails: 35 | self.insert_contacts(first_name=None, last_name=None, title=title, email=email) 36 | # extract and add names to contacts 37 | names = result['Meta']['Names'] 38 | if names is None: names = [] 39 | for name in names: 40 | fname, mname, lname = parse_name(name['Name']) 41 | self.insert_contacts(first_name=fname, middle_name=mname, last_name=lname, title=title) 42 | # extract and consolidate hosts and associated technology data 43 | data = {} 44 | for path in result['Result']['Paths']: 45 | domain = path['Domain'] 46 | subdomain = path['SubDomain'] 47 | host = subdomain if domain in subdomain else '.'.join(filter(len, [subdomain, domain])) 48 | if not host in data: data[host] = [] 49 | data[host] += path['Technologies'] 50 | for host in data: 51 | # add host to hosts 52 | # *** might domain integrity issues here *** 53 | domain = '.'.join(host.split('.')[-2:]) 54 | if domain != host: 55 | self.insert_hosts(host) 56 | # process hosts and technology data 57 | if self.options['show_all']: 58 | for host in data: 59 | self.heading(host, level=0) 60 | # display technologies 61 | if data[host]: 62 | self.output(self.ruler*50) 63 | for item in data[host]: 64 | for tag in item: 65 | self.output(f"{tag}: {textwrap.fill(self.to_unicode_str(item[tag]), 100, initial_indent='', subsequent_indent=self.spacer*2)}") 66 | self.output(self.ruler*50) 67 | -------------------------------------------------------------------------------- /modules/recon/locations-pushpins/flickr.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from datetime import datetime 3 | import json 4 | 5 | class Module(BaseModule): 6 | 7 | meta = { 8 | 'name': 'Flickr Geolocation Search', 9 | 'author': 'Tim Tomes (@lanmaster53)', 10 | 'version': '1.0', 11 | 'description': 'Searches Flickr for media in the specified proximity to a location.', 12 | 'required_keys': ['flickr_api'], 13 | 'comments': ( 14 | 'Radius must be greater than zero and less than 32 kilometers.', 15 | ), 16 | 'query': 'SELECT DISTINCT latitude || \',\' || longitude FROM locations WHERE latitude IS NOT NULL AND longitude IS NOT NULL', 17 | 'options': ( 18 | ('radius', 1, True, 'radius in kilometers'), 19 | ), 20 | } 21 | 22 | def module_run(self, points): 23 | api_key = self.keys.get('flickr_api') 24 | rad = self.options['radius'] 25 | url = 'https://api.flickr.com/services/rest/' 26 | for point in points: 27 | self.heading(point, level=0) 28 | lat = point.split(',')[0] 29 | lon = point.split(',')[1] 30 | payload = {'method': 'flickr.photos.search', 'format': 'json', 'api_key': api_key, 'lat': lat, 'lon': lon, 'has_geo': 1, 'min_taken_date': '1990-01-01 00:00:00', 'extras': 'date_upload,date_taken,owner_name,geo,url_t,url_m', 'radius': rad, 'radius_units':'km', 'per_page': 500} 31 | processed = 0 32 | while True: 33 | resp = self.request('GET', url, params=payload) 34 | jsonobj = json.loads(resp.text[14:-1]) 35 | # check for, and exit on, an erroneous request 36 | if jsonobj['stat'] == 'fail': 37 | self.error(jsonobj['message']) 38 | break 39 | if not processed: 40 | self.output(f"Collecting data for ~{jsonobj['photos']['total']} total photos...") 41 | for photo in jsonobj['photos']['photo']: 42 | latitude = photo['latitude'] 43 | longitude = photo['longitude'] 44 | if not all((latitude, longitude)): 45 | continue 46 | source = 'Flickr' 47 | screen_name = photo['owner'] 48 | profile_name = photo['ownername'] 49 | profile_url = f"http://flickr.com/photos/{screen_name}" 50 | try: 51 | media_url = photo['url_m'] 52 | except KeyError: 53 | media_url = photo['url_t'].replace('_t.', '.') 54 | thumb_url = photo['url_t'] 55 | message = photo['title'] 56 | try: 57 | time = datetime.strptime(photo['datetaken'], '%Y-%m-%d %H:%M:%S') 58 | except ValueError: 59 | time = datetime(1970, 1, 1) 60 | self.insert_pushpins(source, screen_name, profile_name, profile_url, media_url, thumb_url, message, latitude, longitude, time) 61 | processed += len(jsonobj['photos']['photo']) 62 | self.verbose(f"{processed} photos processed.") 63 | if jsonobj['photos']['page'] >= jsonobj['photos']['pages']: 64 | break 65 | payload['page'] = jsonobj['photos']['page'] + 1 66 | -------------------------------------------------------------------------------- /modules/recon/domains-hosts/brute_hosts.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.resolver import ResolverMixin 3 | from recon.mixins.threads import ThreadingMixin 4 | import dns.resolver 5 | import os 6 | 7 | class Module(BaseModule, ResolverMixin, ThreadingMixin): 8 | 9 | meta = { 10 | 'name': 'DNS Hostname Brute Forcer', 11 | 'author': 'Tim Tomes (@lanmaster53)', 12 | 'version': '1.0', 13 | 'description': 'Brute forces host names using DNS. Updates the \'hosts\' table with the results.', 14 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 15 | 'options': ( 16 | ('wordlist', os.path.join(BaseModule.data_path, 'hostnames.txt'), True, 'path to hostname wordlist'), 17 | ), 18 | 'files': ['hostnames.txt'], 19 | } 20 | 21 | def module_run(self, domains): 22 | with open(self.options['wordlist']) as fp: 23 | words = fp.read().split() 24 | resolver = self.get_resolver() 25 | for domain in domains: 26 | self.heading(domain, level=0) 27 | wildcard = None 28 | try: 29 | answers = resolver.query(f"*.{domain}") 30 | wildcard = answers.response.answer[0][0] 31 | self.output(f"Wildcard DNS entry found for '{domain}' at '{wildcard}'.") 32 | except (dns.resolver.NoNameservers, dns.resolver.Timeout): 33 | self.error('Invalid nameserver.') 34 | continue 35 | except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): 36 | self.verbose('No Wildcard DNS entry found.') 37 | self.thread(words, domain, resolver, wildcard) 38 | 39 | def module_thread(self, word, domain, resolver, wildcard): 40 | max_attempts = 3 41 | attempt = 0 42 | while attempt < max_attempts: 43 | host = f"{word}.{domain}" 44 | try: 45 | answers = resolver.query(host) 46 | except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): 47 | self.verbose(f"{host} => No record found.") 48 | except dns.resolver.Timeout: 49 | self.verbose(f"{host} => Request timed out.") 50 | attempt += 1 51 | continue 52 | else: 53 | # process answers 54 | if answers.response.answer[0][0] == wildcard: 55 | self.verbose(f"{host} => Response matches the wildcard.") 56 | else: 57 | for answer in answers.response.answer: 58 | for rdata in answer: 59 | if rdata.rdtype in (1, 5): 60 | if rdata.rdtype == 1: 61 | address = rdata.address 62 | self.alert(f"{host} => (A) {address}") 63 | self.insert_hosts(host, address) 64 | if rdata.rdtype == 5: 65 | cname = rdata.target.to_text()[:-1] 66 | self.alert(f"{host} => (CNAME) {cname}") 67 | self.insert_hosts(cname) 68 | # add the host in case a CNAME exists without an A record 69 | self.insert_hosts(host) 70 | # break out of the loop 71 | attempt = max_attempts 72 | -------------------------------------------------------------------------------- /modules/recon/companies-multi/censys_org.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from censys.search import CensysHosts 4 | from censys.common.exceptions import CensysException 5 | 6 | 7 | class Module(BaseModule): 8 | meta = { 9 | "name": "Censys - Hosts by Company", 10 | "author": "Censys, Inc. ", 11 | "version": 2.1, 12 | "description": ( 13 | "Retrieves hosts for a company's ASN. This module queries the" 14 | " 'autonomous_system.name' field and updates the 'hosts' and" 15 | " 'ports' tables with the results." 16 | ), 17 | "query": ( 18 | "SELECT DISTINCT company FROM companies WHERE company IS NOT NULL" 19 | ), 20 | "required_keys": ["censysio_id", "censysio_secret"], 21 | "options": [ 22 | ( 23 | "virtual_hosts", 24 | "EXCLUDE", 25 | False, 26 | "Whether to include virtual hosts in the results", 27 | ), 28 | ( 29 | "per_page", 30 | "100", 31 | False, 32 | "The number of results to return per page", 33 | ), 34 | ( 35 | "pages", 36 | "1", 37 | False, 38 | "The number of pages to retrieve", 39 | ), 40 | ], 41 | "dependencies": ["censys>=2.1.2"], 42 | } 43 | 44 | def module_run(self, companies): 45 | api_id = self.get_key("censysio_id") 46 | api_secret = self.get_key("censysio_secret") 47 | c = CensysHosts(api_id, api_secret) 48 | for company in companies: 49 | company = company.strip('"') 50 | self.heading(company, level=0) 51 | try: 52 | query = c.search( 53 | f'autonomous_system.name:"{company}"', 54 | per_page=int(self.options.get("PER_PAGE", "100")), 55 | pages=int(self.options.get("PAGES", "1")), 56 | virtual_hosts=self.options.get("VIRTUAL_HOSTS", "EXCLUDE"), 57 | ) 58 | except CensysException: 59 | self.print_exception() 60 | continue 61 | for hit in query(): 62 | ip = hit["ip"] 63 | name = hit.get("name") 64 | if name: 65 | self.insert_domains( 66 | domain=name, notes="+".join((ip, name)) 67 | ) 68 | common_kwargs = { 69 | "ip_address": ip, 70 | "host": name, 71 | } 72 | location = hit.get("location", {}) 73 | coords = location.get("coordinates", {}) 74 | self.insert_hosts( 75 | region=location.get("continent"), 76 | country=location.get("country"), 77 | latitude=coords.get("latitude"), 78 | longitude=coords.get("longitude"), 79 | **common_kwargs, 80 | ) 81 | for service in hit.get("services", []): 82 | self.insert_ports( 83 | port=service["port"], 84 | protocol=service["transport_protocol"], 85 | notes=service["service_name"], 86 | **common_kwargs, 87 | ) 88 | -------------------------------------------------------------------------------- /modules/recon/domains-vulnerabilities/ghdb.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.mixins.search import GoogleWebMixin 3 | from itertools import groupby 4 | from urllib.parse import urlparse, parse_qs 5 | import json 6 | import os 7 | 8 | 9 | def _optionize(s): 10 | return f"ghdb_{s.replace(' ', '_').lower()}" 11 | 12 | 13 | def _build_options(ghdb): 14 | categories = [] 15 | for key, group in groupby([x['category'] for x in sorted(ghdb, key=lambda x: x['category'])]): 16 | categories.append((_optionize(key), False, True, f"enable/disable the {len(list(group))} dorks in this " 17 | f"category")) 18 | return categories 19 | 20 | 21 | class Module(BaseModule, GoogleWebMixin): 22 | 23 | with open(os.path.join(BaseModule.data_path, 'ghdb.json')) as fp: 24 | ghdb = json.load(fp) 25 | 26 | meta = { 27 | 'name': 'Google Hacking Database', 28 | 'author': 'Tim Tomes (@lanmaster53)', 29 | 'version': '1.1', 30 | 'description': 'Searches for possible vulnerabilites in a domain by leveraging the Google Hacking ' 31 | 'Database (GHDB) and the \'site\' search operator. Updates the \'vulnerabilities\' table ' 32 | 'with the results.', 33 | 'comments': ( 34 | 'Offensive Security no longer provides access to the GHDB for Recon-ng. The included list was last ' 35 | 'updated on 8/1/2016.', 36 | ), 37 | 'query': 'SELECT DISTINCT domain FROM domains WHERE domain IS NOT NULL', 38 | 'options': [ 39 | ('dorks', None, False, 'file containing an alternate list of Google dorks'), 40 | ] + _build_options(ghdb), 41 | 'files': [ 42 | 'ghdb.json', 43 | ], 44 | } 45 | 46 | def module_run(self, domains): 47 | dorks = self.ghdb 48 | # use alternate list of dorks if the option is set 49 | if self.options['dorks'] and os.path.exists(self.options['dorks']): 50 | with open(self.options['dorks']) as fp: 51 | dorks = [x.strip() for x in fp.readlines()] 52 | for domain in domains: 53 | self.heading(domain, level=0) 54 | base_query = f"site:{domain}" 55 | for dork in dorks: 56 | # build query based on alternate list 57 | if isinstance(dork, str): 58 | query = ' '.join((base_query, dork)) 59 | self._search(query) 60 | # build query based on ghdb entry 61 | elif isinstance(dork, dict): 62 | if not dork['querystring']: 63 | continue 64 | if self.options[_optionize(dork['category'])]: 65 | # parse the query string to extract the dork syntax 66 | parsed = urlparse(dork['querystring']) 67 | params = parse_qs(parsed.query) 68 | # unparsable url 69 | if 'q' not in params: 70 | continue 71 | query = ' '.join((base_query, params['q'][0])) 72 | self._search(query) 73 | 74 | def _search(self, query): 75 | for result in self.search_google_web(query): 76 | host = urlparse(result).netloc 77 | data = { 78 | 'host': host, 79 | 'reference': query, 80 | 'example': result, 81 | 'category': 'Google Dork', 82 | } 83 | self.insert_vulnerabilities(**data) 84 | -------------------------------------------------------------------------------- /modules/exploitation/injection/command_injector.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from urllib.parse import unquote_plus 3 | 4 | class Module(BaseModule): 5 | 6 | meta = { 7 | 'name': 'Remote Command Injection Shell Interface', 8 | 'author': 'Tim Tomes (@lanmaster53)', 9 | 'version': '1.0', 10 | 'description': 'Provides a shell interface for remote command injection flaws in web applications.', 11 | 'options': ( 12 | ('base_url', None, True, 'the target resource url excluding any parameters'), 13 | ('parameters', None, True, 'the query parameters with \'\' signifying the value of the vulnerable parameter'), 14 | ('basic_user', None, False, 'username for basic authentication'), 15 | ('basic_pass', None, False, 'password for basic authentication'), 16 | ('cookie', None, False, 'cookie string containing authenticated session data'), 17 | ('post', False, True, 'set the request method to POST'), 18 | ('mark_start', None, False, 'string to match page content preceding the command output'), 19 | ('mark_end', None, False, 'string to match page content following the command output'), 20 | ), 21 | } 22 | 23 | def help(self): 24 | return 'Type \'exit\' or \'ctrl-c\' to exit the shell.' 25 | 26 | def parse_params(self, params): 27 | params = params.split('&') 28 | params = [param.split('=') for param in params] 29 | return [(unquote_plus(param[0]), unquote_plus(param[1])) for param in params] 30 | 31 | def module_run(self): 32 | base_url = self.options['base_url'] 33 | base_params = self.options['parameters'] 34 | username = self.options['basic_user'] 35 | password = self.options['basic_pass'] 36 | cookie = self.options['cookie'] 37 | start = self.options['mark_start'] 38 | end = self.options['mark_end'] 39 | 40 | # process authentication 41 | kwargs = { 42 | headers: {'Cookie': cookie} if cookie else {}, 43 | auth: (username, password) if username and password else (), 44 | } 45 | 46 | # set the request method 47 | method = 'POST' if self.options['post'] else 'GET' 48 | 49 | print('Type \'help\' or \'?\' for assistance.') 50 | while True: 51 | # get command from the terminal 52 | cmd = input("cmd> ") 53 | if cmd.lower() == 'exit': 54 | return 55 | elif cmd.lower() in ['help', '?']: 56 | print(self.help()) 57 | continue 58 | # build the payload from the base_params string 59 | payload = {} 60 | params = self.parse_params(base_params.replace('', cmd)) 61 | for param in params: 62 | payload[param[0]] = param[1] 63 | # send the request 64 | if method == 'GET': 65 | kwargs['params'] = payload 66 | else: 67 | kwargs['data'] = payload 68 | resp = self.request('GET', base_url, **kwargs) 69 | # process the response 70 | output = resp.text 71 | if start and end: 72 | try: 73 | output = output[output.index(start)+len(start):] 74 | except ValueError: 75 | self.error('Invalid start marker.') 76 | try: 77 | output = output[:output.index(end)] 78 | except ValueError: 79 | self.error('Invalid end marker.') 80 | print(output.strip()) 81 | -------------------------------------------------------------------------------- /data/template_media.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pushpin - Media 5 | 6 | 144 | 145 | 146 | 147 | 148 |
    %s
    149 | 150 | 151 | -------------------------------------------------------------------------------- /modules/recon/contacts-contacts/abc.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | 3 | from recon.mixins.threads import ThreadingMixin 4 | 5 | from bs4 import BeautifulSoup 6 | 7 | 8 | class Module(BaseModule, ThreadingMixin): 9 | 10 | meta = { 11 | "name": "Advance Background Check Lookup", 12 | "author": "Cam Barts (@cam-barts)", 13 | "version": "1.0", 14 | "description": "Checks names at advancedbackgroundchecks.com.", 15 | "dependencies": ["beautifulsoup4"], 16 | "comments": ("Inspiration drawn from @xillwillx skiptracer module https://github.com/xillwillx/skiptracer/blob/master/plugins/advance_background_checks.py"), 17 | "query": "SELECT DISTINCT first_name, last_name, region FROM contacts WHERE first_name IS NOT NULL AND last_name IS NOT NULL", 18 | "options": ( 19 | ("location", "", True, "location to search (city, state, or zip)"), 20 | ), 21 | } 22 | 23 | def module_run(self, people): 24 | url_names = [] 25 | for person in people: 26 | name = f"{person[0]}-{person[1]}" 27 | if person[2]: 28 | region = f"{person[2]}".replace(" ", "-") 29 | else: 30 | region = self.options["location"] 31 | url = f"https://www.advancedbackgroundchecks.com/name/{name}_{region}" 32 | url_names.append((url, name)) 33 | self.thread(url_names) 34 | 35 | def module_thread(self, url_name): 36 | url = url_name[0] 37 | name = url_name[1].replace("-", " ") 38 | 39 | resp = self.request("GET", url) 40 | soup = BeautifulSoup(resp.content, features="html.parser") 41 | 42 | # Output number of results for person 43 | num_results = soup.find("h1", attrs={"class": "list-results-header"}).text 44 | clean_num_results = " ".join(num_results.split()) 45 | self.alert(clean_num_results) 46 | 47 | # If only one result, go to that persons page any grab goodies 48 | if int(clean_num_results.split()[0]) > 1: 49 | self.alert(f"Too many results for {name}, try to narrow your search") 50 | self.alert(url) 51 | return 52 | elif int(clean_num_results.split()[0]) == 0: 53 | self.alert(f"No results for {name}") 54 | return 55 | else: 56 | link = soup.find("a", attrs={"class": "link-to-details"}) 57 | new_url = f"https://www.advancedbackgroundchecks.com{link['href']}" 58 | person_resp = self.request("GET", new_url) 59 | person_soup = BeautifulSoup(person_resp.content, features="html.parser") 60 | 61 | # Grab current address 62 | current_address_tag = person_soup.find("p", attrs={"class": "address-link"}) 63 | current_address = " ".join( 64 | current_address_tag.text.split()[1:] 65 | ) # The first word is "map", so skip that 66 | 67 | self.insert_locations(street_address=current_address) 68 | 69 | # Grab emails 70 | emails_tag = person_soup.find("div", attrs={"class": "detail-box-email"}) 71 | emails = list(emails_tag.stripped_strings) 72 | 73 | # Add contacts 74 | for email in emails: 75 | self.insert_contacts( 76 | first_name=name.split()[0], 77 | last_name=name.split()[1], 78 | region=current_address, 79 | email=email, 80 | country="United States", 81 | ) 82 | 83 | # Update regions 84 | self.query( 85 | "UPDATE contacts SET region = ? WHERE first_name = ? AND last_name = ?", 86 | (current_address, name.split()[0], name.split()[1]), 87 | ) 88 | -------------------------------------------------------------------------------- /modules/recon/contacts-profiles/fullcontact.py: -------------------------------------------------------------------------------- 1 | from recon.core.module import BaseModule 2 | from recon.utils.parsers import parse_name 3 | from time import sleep 4 | 5 | 6 | def find(key, dictionary): 7 | for k, v in dictionary.items(): 8 | if k == key: 9 | yield v 10 | elif isinstance(v, dict): 11 | for result in find(key, v): 12 | yield result 13 | elif isinstance(v, list): 14 | for d in v: 15 | for result in find(key, d): 16 | yield result 17 | 18 | 19 | class Module(BaseModule): 20 | 21 | meta = { 22 | 'name': 'FullContact Contact Enumerator', 23 | 'author': 'Tim Tomes (@lanmaster53) and Cam Barts (@cam-barts)', 24 | 'version': '1.1', 25 | 'description': 'Harvests contact information and profiles from the fullcontact.com API using email addresses ' 26 | 'as input. Updates the \'contacts\' and \'profiles\' tables with the results.', 27 | 'required_keys': ['fullcontact_api'], 28 | 'query': 'SELECT DISTINCT email FROM contacts WHERE email IS NOT NULL', 29 | } 30 | 31 | def module_run(self, entities): 32 | api_key = self.keys.get('fullcontact_api') 33 | base_url = 'https://api.fullcontact.com/v3/person.enrich' 34 | while entities: 35 | entity = entities.pop(0) 36 | payload = {'email': entity} 37 | headers = {'Authorization': 'Bearer ' + api_key} 38 | resp = self.request('POST', base_url, json=payload, headers=headers) 39 | if resp.status_code == 200: 40 | 41 | # parse contact information 42 | name = resp.json().get('fullName') 43 | if name: 44 | first_name, middle_name, last_name = parse_name(name) 45 | self.alert(name) 46 | emails = [entity] 47 | new_emails = resp.json()['details'].get('emails') or [] 48 | for email in new_emails: 49 | emails.append(email['value']) 50 | self.alert(email['value']) 51 | title = resp.json().get('title') 52 | organization = resp.json().get('organization') 53 | if title and organization: 54 | title = f"{title} at {organization}" 55 | elif organization: 56 | title = f"Employee at {organization}" 57 | if title: 58 | self.alert(title) 59 | 60 | # parse location 61 | region = resp.json().get('location') 62 | if region: 63 | self.alert(region) 64 | 65 | # insert contacts 66 | for email in emails: 67 | self.insert_contacts(first_name=first_name, middle_name=middle_name, last_name=last_name, title=title, 68 | email=email, region=region) 69 | 70 | # parse and insert profiles 71 | for resource in ['twitter', 'linkedin', 'facebook']: 72 | url = resp.json().get(resource) 73 | if url: 74 | username = url.split('/')[-1] 75 | self.alert(url) 76 | self.insert_profiles(username=username, url=url, resource=resource, category='social') 77 | 78 | elif resp.status_code == 202: 79 | # add emails queued by fullcontact back to the list 80 | entities.append(entity) 81 | self.output(f"{entity} queued and added back to the list.") 82 | else: 83 | self.output(f"{entity} - {resp.json()['message']}") 84 | # 600 requests per minute api rate limit 85 | sleep(.1) 86 | -------------------------------------------------------------------------------- /modules/recon/contacts-credentials/hibp_paste.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from recon.core.module import BaseModule 5 | from requests.exceptions import ConnectionError 6 | from urllib.parse import quote_plus 7 | 8 | 9 | class Module(BaseModule): 10 | 11 | meta = { 12 | 'name': 'Have I been pwned? Paste Search', 13 | 'author': 'Tim Tomes (@lanmaster53) and Geoff Pamerleau (@_geoff_p_)', 14 | 'version': '1.1', 15 | 'description': 'Leverages the haveibeenpwned.com API to determine if email addresses have been published to ' 16 | 'various paste sites. Adds compromised email addresses to the \'credentials\' table.', 17 | 'comments': ( 18 | 'Paste sites supported: Pastebin, Pastie, Slexy, Ghostbin, QuickLeak, JustPaste, AdHocUrl, and OptOut.' 19 | 'The HIBP API is rate limited to 1 request per 1.5 seconds.', 20 | ), 21 | 'query': 'SELECT DISTINCT email FROM contacts WHERE email IS NOT NULL', 22 | 'required_keys': ['hibp_api'], 23 | 'options': ( 24 | ('download', True, True, 'download pastes'), 25 | ), 26 | } 27 | 28 | def module_run(self, accounts): 29 | # check back often for new paste sources 30 | headers = {'hibp-api-key': self.keys['hibp_api']} 31 | sites = { 32 | 'Pastebin': 'http://pastebin.com/raw.php?i={}', 33 | 'Pastie': 'http://pastie.org/pastes/{}/text', 34 | 'Slexy': 'http://slexy.org/raw/{}', 35 | 'Ghostbin': 'https://ghostbin.com/paste/{}/raw', 36 | 'QuickLeak': 'http://www.quickleak.ir/{}', 37 | 'JustPaste': 'https://justpaste.it/{}', 38 | 'AdHocUrl': '{}', 39 | } 40 | # retrieve status 41 | base_url = 'https://haveibeenpwned.com/api/v3/{}/{}' 42 | endpoint = 'pasteaccount' 43 | for account in accounts: 44 | resp = self.request('GET', base_url.format(endpoint, quote_plus(account)), headers=headers) 45 | rcode = resp.status_code 46 | if rcode == 404: 47 | self.verbose(f"{account} => Not Found.") 48 | elif rcode == 400: 49 | self.error(f"{account} => Bad Request.") 50 | continue 51 | else: 52 | for paste in resp.json(): 53 | download = False 54 | fileurl = paste['Id'] 55 | if paste['Source'] in sites: 56 | fileurl = sites[paste['Source']].format(paste['Id']) 57 | download = self.options['download'] 58 | elif self.options['download']: 59 | self.alert(f"Download not available for {paste['Source']} pastes.") 60 | self.alert(f"{account} => Paste found! Seen in a {paste['Source']} on {paste['Date']} ({fileurl}).") 61 | if download: 62 | try: 63 | resp = self.request('GET', fileurl) 64 | except ConnectionError: 65 | self.alert(f"Paste could not be downloaded ({fileurl}).") 66 | 67 | if resp.status_code == 200: 68 | filepath = f"{self.workspace}/{_safe_file_name(fileurl)}.txt" 69 | if not os.path.exists(filepath): 70 | dl = open(filepath, 'wb') 71 | dl.write(resp.content) 72 | dl.close() 73 | self.verbose(f"Paste stored at '{filepath}'.") 74 | else: 75 | self.alert(f"Paste could not be downloaded ({fileurl}).") 76 | self.insert_credentials(account) 77 | time.sleep(1.6) 78 | 79 | def _safe_file_name(s): 80 | return "".join(c for c in s if c.isalnum()).rstrip() 81 | --------------------------------------------------------------------------------