-
Notifications
You must be signed in to change notification settings - Fork 113
/
odin.py
executable file
·332 lines (295 loc) · 16.2 KB
/
odin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
:::==== :::==== ::: :::= ===
::: === ::: === ::: :::=====
=== === === === === ========
=== === === === === === ====
====== ======= === === ===
Developer: Chris "cmaddy" Maddalena
Version: 2.0.0 "Huginn"
Description: Observation, Detection, and Investigation of Networks
ODIN was designed to assist with OSINT automation for penetration testing clients and
their networks, both the types with IP address and social. Provide a client's name and
some domains to gather information from sources like RDAP, DNS, Shodan, and
so much more.
ODIN is made possible through the help, input, and work provided by others. Therefore,
this project is entirely open source and available to all to use/modify.
"""
import os
import click
from multiprocess import Process,Manager
from lib import reporter,asciis,verification,htmlreporter,grapher,helpers
VERSION = "2.0.0"
CODENAME = "HUGINN"
def setup_reports(client):
"""Function to create a reports directory structure for the target organization."""
if not os.path.exists("reports/{}".format(client)):
try:
os.makedirs("reports/{}".format(client))
os.makedirs("reports/{}/screenshots".format(client))
os.makedirs("reports/{}/file_downloads".format(client))
os.makedirs("reports/{}/html_report".format(client))
except OSError as error:
click.secho("[!] Could not create the reports directory!",fg="red")
click.secho("L.. Details: {}".format(error),fg="red")
# Setup a class for CLICK
class AliasedGroup(click.Group):
"""Allows commands to be called by their first unique character."""
def get_command(self,ctx,cmd_name):
"""
Allows commands to be called by their first unique character
:param ctx: Context information from click
:param cmd_name: Calling command name
:return:
"""
command = click.Group.get_command(self,ctx,cmd_name)
if command is not None:
return command
matches = [x for x in self.list_commands(ctx)
if x.startswith(cmd_name)]
if not matches:
return None
elif len(matches) == 1:
return click.Group.get_command(self,ctx,matches[0])
ctx.fail("Too many matches: %s" % ", ".join(sorted(matches)))
# That's right, we support -h and --help! Not using -h for an argument like 'host'! ;D
CONTEXT_SETTINGS = dict(help_option_names=['-h','--help'],max_content_width=200)
@click.group(cls=AliasedGroup,context_settings=CONTEXT_SETTINGS)
# Note: The following function descriptors will look weird and some will contain '\n' in spots.
# This is necessary for CLICK. These are displayed with the help info and need to be written
# just like we want them to be displayed in the user's terminal. Whitespace really matters.
def odin():
"""
Welcome to ODIN! To use ODIN, select a module you wish to run. Functions are split into modules
to support a few different use cases.\n
Run 'odin.py <MODULE> --help' for more information on a specific module.
"""
# Everything starts here
pass
# The OSINT module -- This is the primary module that does all the stuff
# Basic, required arguments
@odin.command(name='osint',short_help="The full OSINT suite of tools will be run (see README).")
@click.option('-o','--organization',help='The target client, such as "ABC Company," to use for \
report titles and searches for domains and cloud storage buckets.',required=True)
@click.option('-d','--domain',help="The target's primary domain, such as example.com. Use \
whatever the target uses for email and their main website. Provide additional domains in a scope \
file using --scope-file.",required=True)
# Optional arguments
@click.option('-sf','--scope-file',type=click.Path(exists=True,readable=True,\
resolve_path=True),help="A text file containing additional domain names you want to include. IP \
addresses can also be provided, if necessary. List each one on a new line.",required=False)
@click.option('--whoxy-limit',default=10,help="The maximum number of domains discovered via \
reverse WHOIS that ODIN will resolve and use when searching services like Censys and Shodan. \
You may get hundreds of results from reverse WHOIS, so this is intended to save time and \
API credits. Default is 10 domains and setting it above maybe 20 or 30 is not recommended. \
It is preferable to perform a search using a tool like Vincent Yiu's DomLink and then provide \
the newly discovered domains in your scope file with --scope-file.")
@click.option('--typo',is_flag=True,help="Generate a list of lookalike domain names for the \
provided domain (--domain), check if they have been registered, and then check those domains \
against URLVoid and Cymon.io to see if the domains or associated IP addresses have been \
flagged as malicious.")
# File searching arguments
@click.option('--files',is_flag=True,help="Use this option to use Google to search for files \
under the provided domain (--domain), download files, and extract metadata.")
@click.option('-e','--ext',default="all",help="File extensions to look for with --file. \
Default is 'all' or you can pick from key, pdf, doc, docx, xls, xlsx, and ppt.")
# Cloud-related arguments
@click.option('-w','--aws',help="A list of additional keywords to be used when searching for \
cloud sotrage buckets.",type=click.Path(exists=True,readable=True,resolve_path=True))
@click.option('-wf','--aws-fixes',help="A list of strings to be added to the start and end of \
the cloud storage bucket names.",type=click.Path(exists=True,readable=True,resolve_path=True))
# Reporting-related arguments
@click.option('--html',is_flag=True,help="Create an HTML report at the end for easy browsing.")
@click.option('--graph',is_flag=True,help="Create a Neo4j graph database from the completed \
SQLite3 database.")
@click.option('--nuke',is_flag=True,help="Clear the Neo4j project before converting the \
database. This is only used with --graph.")
@click.option('--screenshots',is_flag=True,help="Attempt to take screenshots of discovered \
web services.")
@click.option('--unsafe',is_flag=True,help="Adding this flag will spawn the headless Chrome \
browser with the --no-sandbox command line flag. This is NOT recommended for any users who are \
NOT running ODIN on a Kali Linux VM as root. Chrome will not run as the root user on Kali \
without this option.")
# Pass the above arguments on to your osint function
@click.pass_context
def osint(self,organization,domain,files,ext,scope_file,aws,aws_fixes,html,
screenshots,graph,nuke,whoxy_limit,typo,unsafe):
"""
The OSINT toolkit:
This is ODIN's primary module. ODIN will take the tagret organization, domain, and other data
provided and hunt for information. On the human side, ODIN looks for employee names,
email addresses, and social media profiles. Names and emails are cross-referenced with
HaveIBeenPwned, Twitter's API, and search engines to collect additional information.
ODIN also uses various tools and APIs to collect information on the provided IP addresses
and domain names, including things like DNS and IP address history.
View the wiki for the full details, reporting information, and lists of API keys.
Note: If providing any IP addresses in a scope file, acceptable IP addresses/ranges include:
* Single Address: 8.8.8.8
* Basic CIDR: 8.8.8.0/24
* Nmap-friendly Range: 8.8.8.8-10
* Underscores? OK: 8.8.8.8_8.8.8.10
"""
click.clear()
click.secho(asciis.print_art(),fg="magenta")
click.secho("\tRelease v{}, {}".format(VERSION,CODENAME),fg="magenta")
click.secho("[+] OSINT Module Selected: ODIN will run all recon modules.",fg="green")
# Perform prep work for reporting
setup_reports(organization)
report_path = "reports/{}/".format(organization)
output_report = report_path + "OSINT_DB.db"
if __name__ == "__main__":
# Create manager server to handle variables shared between jobs
manager = Manager()
ip_list = manager.list()
domain_list = manager.list()
rev_domain_list = manager.list()
# Create reporter object and generate lists of everything, just IP addresses, and just domains
browser = helpers.setup_headless_chrome(unsafe)
report = reporter.Reporter(organization,report_path,output_report,browser)
report.create_tables()
scope,ip_list,domain_list = report.prepare_scope(ip_list,domain_list,scope_file,domain)
# Create some jobs and put Python to work!
# Job queue 1 is for the initial phase
jobs = []
# Job queue 2 is used for jobs using data from job queue 1
more_jobs = []
# Job queue 3 is used for jobs that take a while and use the progress bar, i.e. AWS enum
even_more_jobs = []
# Phase 1 jobs
company_info = Process(name="Company Info Collector",
target=report.create_company_info_table,
args=(domain,))
jobs.append(company_info)
employee_report = Process(name="Employee Hunter",
target=report.create_people_table,
args=(domain_list,rev_domain_list,organization))
jobs.append(employee_report)
domain_report = Process(name="Domain and IP Hunter",
target=report.create_domain_report_table,
args=(organization,scope,ip_list,domain_list,rev_domain_list,whoxy_limit))
jobs.append(domain_report)
# Phase 2 jobs
shodan_report = Process(name="Shodan Hunter",
target=report.create_shodan_table,
args=(ip_list,domain_list))
more_jobs.append(shodan_report)
if typo:
lookalike_report = Process(name="Lookalike Domain Reviewer",
target=report.create_lookalike_table,
args=(organization,domain))
more_jobs.append(lookalike_report)
if screenshots:
take_screenshots = Process(name="Screenshot Snapper",
target=report.capture_web_snapshots,
args=(report_path,browser))
more_jobs.append(take_screenshots)
if files:
files_report = Process(name="File Hunter",
target=report.create_metadata_table,
args=(domain,ext,report_path))
more_jobs.append(files_report)
# Phase 3 jobs
cloud_report = Process(name="Cloud Hunter",
target=report.create_cloud_table,
args=(organization,domain,aws,aws_fixes))
even_more_jobs.append(cloud_report)
# Process the lists of jobs in phases, starting with phase 1
click.secho("[+] Beginning initial discovery phase! This could take some time...",fg="green")
for job in jobs:
click.secho("[+] Starting new process: {}".format(job.name),fg="green")
job.start()
for job in jobs:
job.join()
# Wait for phase 1 and then begin phase 2 jobs
click.secho("[+] Initial discovery is complete! Proceeding with additional queries...",fg="green")
for job in more_jobs:
click.secho("[+] Starting new process: {}".format(job.name),fg="green")
job.start()
for job in more_jobs:
job.join()
# Wait for phase 2 and then begin phase 3 jobs
click.secho("[+] Final phase: checking the cloud and web services...",fg="green")
for job in even_more_jobs:
click.secho("[+] Starting new process: {}".format(job.name),fg="green")
job.start()
for job in even_more_jobs:
job.join()
# All jobs are done, so close out the SQLIte3 database connection
report.close_out_reporting()
click.secho("[+] Job's done! Your results are in {} and can be viewed and queried with \
any SQLite browser.".format(output_report),fg="green")
# Perform additional tasks depending on the user's command line options
if graph:
graph_reporter = grapher.Grapher(output_report)
click.secho("[+] Loading ODIN database file {} for conversion to Neo4j".format(output_report),fg="green")
if nuke:
if click.confirm(click.style("[!] You set the --nuke option. This wipes out all nodes for a \
fresh start. Proceed?",fg="red"),default=True):
try:
graph_reporter.clear_neo4j_database()
click.secho("[+] Database successfully wiped!\n",fg="green")
except Exception as error:
click.secho("[!] Failed to clear the database! Check the Neo4j console and \
your configuration and try running grapher.py again.",fg="red")
click.secho("L.. Details: {}".format(error),fg="red")
else:
click.secho("[!] You can convert your database to a graph database later. \
Run lib/grapher.py with the appropriate options.",fg="red")
try:
graph_reporter.convert()
except Exception as error:
click.secho("[!] Failed to convert the database! Check the Neo4j console and \
your configuration and try running grapher.py again.",fg="red")
click.secho("L.. Details: {}".format(error),fg="red")
if html:
click.secho("\n[+] Creating the HTML report using {}.".format(output_report),fg="green")
try:
html_reporter = htmlreporter.HTMLReporter(organization,report_path + "/html_report/",output_report)
html_reporter.generate_full_report()
except Exception as error:
click.secho("[!] Failed to create the HTML report!",fg="red")
click.secho("L.. Details: {}".format(error),fg="red")
# The VERIFY module -- No OSINT, just a way to check a ownership of a list of IPs
@odin.command(name='verify',short_help="This module assists with verifying ownership of a list \
of IP addresses. This returns a csv file with SSL cert, WHOIS, and other data for verification.")
@click.option('-o','--organization',help='The target client, such as "ABC Company," to use for \
report titles and some keyword searches.',required=True)
@click.option('-sf','--scope-file',help="Name of the file with your IP addresses.",\
type=click.Path(exists=True,readable=True,resolve_path=True),required=True)
@click.option('-r','--report',default="Verification.csv",help="Output file (CSV) for the \
findings.")
# Pass the above arguments on to your verify function
@click.pass_context
def verify(self,organization,scope_file,report):
"""
The Verify module:
Uses reverse DNS, ARIN, and SSL/TLS certificate information to help you verify ownership of a
list of IP addresses.
This is only for verifying IP addresses. Domains may not have public ownership information
available. Compare the IP ownership information from ARIN and certificate information to what
you know about the presumed owner to determine ownership.
Acceptable IP addresses/ranges include:
* Single Address: 8.8.8.8
* Basic CIDR: 8.8.8.0/24
* Nmap-friendly Range: 8.8.8.8-10
* Underscores? OK: 8.8.8.8_8.8.8.10
"""
click.secho(asciis.print_art(),fg="magenta")
click.secho("\tRelease v{}, {}".format(VERSION,CODENAME),fg="magenta")
click.secho("[+] Scope Verification Module Selected: ODIN will attempt to verify who owns \
the provided IP addresses.",fg="green")
setup_reports(organization)
report_path = "reports/{}/{}".format(organization,report)
expanded_scope = []
results = {}
try:
verification.prepare_scope(scope_file,expanded_scope)
verification.perform_whois(expanded_scope,results)
verification.print_output(results,report_path)
except Exception as error:
click.secho("[!] Verification failed!",fg="red")
click.secho("L.. Details: {}".format(error),fg="red")
click.secho("[+] Job's done! Your identity report is in {}.".format(report_path),fg="green")
if __name__ == "__main__":
odin()