forked from chrismaddalena/ODIN
-
Notifications
You must be signed in to change notification settings - Fork 0
/
odin.py
executable file
·321 lines (268 loc) · 14.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
:::==== :::==== ::: :::= ===
::: === ::: === ::: :::=====
=== === === === === ========
=== === === === === === ====
====== ======= === === ===
Developer: Chris "cmaddy" Maddalena
Version: 1.9.1 "Muninn"
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
from multiprocess import Process, Manager
import click
from colors import red, green, yellow
from lib import reporter, asciis, verification, htmlreporter, grapher
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:
print(red("[!] Could not create the reports directory!"))
print(red("L.. Details: {}".format(error)))
# 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"])
@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 some keyword searches.", 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. Add more domains to your 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 IP addresses and \
domain names you want to include. 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.")
# File searching arguments
@click.option('--files', is_flag=True, help="Use this option to use Google to search for files \
under the provided domain (-d), 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.")
@click.option('-x', '--delete', is_flag=True, help="Set this option if you want the downloaded \
files with --file to be deleted after analysis.")
# Cloud-related arguments
@click.option('-w', '--aws', help="A list of AWS S3 bucket names to validate.", \
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 \
AWS S3 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 used with --graph.")
@click.option('--screenshots', is_flag=True, help="Attempt to take screenshots of discovered \
web services.")
# Pass the above arguments on to your osint function
@click.pass_context
def osint(self, organization, domain, files, ext, delete, scope_file, aws, aws_fixes, html,
screenshots, graph, nuke, whoxy_limit):
"""
The OSINT toolkit:\n
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 README for the full detailsand lists of API keys!
Note: If providing 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()
asciis.print_art()
print(green("[+] OSINT Module Selected: ODIN will run all recon modules."))
verbose = None
if verbose:
print(yellow("[*] Verbose output Enabled -- Enumeration of RDAP contact information \
is enabled, so you may get a lot of it if scope includes a large cloud provider."))
# 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()
# Create reporter object and generate final list, the scope from scope file
report = reporter.Reporter(report_path, output_report)
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 = []
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, organization))
jobs.append(employee_report)
domain_report = Process(name="Domain and IP Address Recon",
target=report.create_domain_report_table,
args=(organization, scope, ip_list, domain_list, whoxy_limit))
jobs.append(domain_report)
shodan_report = Process(name="Shodan Queries",
target=report.create_shodan_table,
args=(ip_list, domain_list))
more_jobs.append(shodan_report)
urlcrazy_report = Process(name="Domain Squatting Recon",
target=report.create_urlcrazy_table,
args=(organization, domain))
more_jobs.append(urlcrazy_report)
cloud_report = Process(name="Cloud Recon",
target=report.create_cloud_table,
args=(organization, domain, aws, aws_fixes))
even_more_jobs.append(cloud_report)
if screenshots:
take_screenshots = Process(name="Screenshot Snapper",
target=report.capture_web_snapshots,
args=(report_path,))
more_jobs.append(take_screenshots)
if files:
files_report = Process(name="File Hunter",
target=report.create_foca_table,
args=(domain, ext, delete, report_path, verbose))
more_jobs.append(files_report)
print(green("[+] Beginning initial discovery phase! This could take some time..."))
for job in jobs:
print(green("[+] Starting new process: {}".format(job.name)))
job.start()
for job in jobs:
job.join()
print(green("[+] Initial discovery is complete! Proceeding with additional queries..."))
for job in more_jobs:
print(green("[+] Starting new process: {}".format(job.name)))
job.start()
for job in more_jobs:
job.join()
print(green("[+] Final phase: checking the cloud and web services..."))
for job in even_more_jobs:
print(green("[+] Starting new process: {}".format(job.name)))
job.start()
for job in even_more_jobs:
job.join()
report.close_out_reporting()
print(green("[+] Job's done! Your results are in {} and can be viewed and queried with \
any SQLite browser.".format(output_report)))
if graph:
graph_reporter = grapher.Grapher(output_report)
print(green("[+] Loading ODIN database file {} for conversion to Neo4j").format(output_report))
if nuke:
confirm = input(red("\n[!] You set the --nuke option. This wipes out all nodes \
for a fresh start. Proceed? (Y\\N) "))
if confirm.lower() == "y":
graph_reporter.clear_neo4j_database()
print(green("[+] Database successfully wiped!\n"))
graph_reporter.convert()
else:
print(red("[!] Then you can convert your database to a graph database later. \
Run lib/grapher.py with the appropriate options."))
else:
graph_reporter.convert()
if html:
print(green("\n[+] Creating the HTML report using {}.".format(output_report)))
html_reporter = htmlreporter.HTMLReporter(organization, report_path + "/html_report/", output_report)
html_reporter.generate_full_report()
# 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.")
@click.option('--cidr', is_flag=True, help="Use if the scoped IPs include any CIDRs.")
# Pass the above arguments on to your verify function
@click.pass_context
def verify(self, scope_file, output, cidr, client):
"""
HERE THERE BE DRAGONS : This code needs updating, so it might be janky.
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
"""
asciis.print_art()
print(green("[+] Scope Verification Module Selected: ODIN will attempt to verify who owns \
the provided IP addresses."))
setup_reports(client)
report = "reports/{}/{}".format(client, output)
ip_list = []
out = {}
try:
verification.prepare_scope(scope_file, ip_list, cidr)
verification.perform_whois(ip_list, out)
verification.print_output(out, report)
except Exception as error:
print(red("[!] Verification failed!"))
print(red("L.. Details: {}".format(error)))
print(green("[+] Job's done! Your identity report is in {}.".format(report)))
if __name__ == "__main__":
odin()