98 | ```
99 |
100 | ## Features
101 | Here's a quick overview of Elyzer's features:
102 | - Print general e-mail informations
103 | - Print relay routing with timestamps
104 | - Print security headers and check if set correctly
105 | - Print interesting headers such as "Envelope-From"
106 | - Print MS-Exchange Headers
107 | - Spoofing / Phishing analyzer with optional passive DNS lookup
108 |
109 | *Spoofing / Phishing detection feature:*
110 |
111 |
112 |
113 | ## To-Do
114 | - [ ] Add JSON output functionality.
115 | - [x] Add a functionality to be able to passively query DNS information to reduce OPSEC concerns.
116 | - [x] Switching entirely to the Driftnet API
117 | - [ ] Optimize my garbage code :D
118 |
119 | ## Community Projects
120 |
121 | Check out this awesome project by [@adriy-be](https://github.com/adriy-be): a WebUI for Elyzer!
Github repo: https://github.com/adriy-be/ElyzerWebUi
122 |
123 | ## Notes
124 | Credits for the *getReceivedFields* & the *getFields* functions goes to "spcnvdr" , Copyright 2020.
125 | Also, thanks to [@triggerfx](https://github.com/triggerfx) for the custom Logo !
126 |
127 | ## Disclaimer
128 | This tool is primarly created for me as a project to enhance my coding skills and start creating some red team / blue team tools. It is not considered to be the most efficient tool out there.
129 |
130 |
--------------------------------------------------------------------------------
/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mochabyte0x/Elyzer/132da05e1830aac57ac7f5f1266c3d725a6c05c2/core/__init__.py
--------------------------------------------------------------------------------
/core/colors.py:
--------------------------------------------------------------------------------
1 | import colorama
2 | from colorama import Fore, Style
3 |
4 | colorama.init(autoreset=True)
5 |
6 | class Colors:
7 | @staticmethod
8 | def red(str):
9 | return Fore.RED + str + Style.RESET_ALL
10 |
11 | @staticmethod
12 | def green(str):
13 | return Fore.GREEN + str + Style.RESET_ALL
14 |
15 | @staticmethod
16 | def yellow(str):
17 | return Fore.YELLOW + str + Style.RESET_ALL
18 |
19 | @staticmethod
20 | def blue(str):
21 | return Fore.BLUE + str + Style.RESET_ALL
22 |
23 | @staticmethod
24 | def magenta(str):
25 | return Fore.MAGENTA + str + Style.RESET_ALL
26 |
27 | @staticmethod
28 | def cyan(str):
29 | return Fore.CYAN + str + Style.RESET_ALL
30 |
31 | @staticmethod
32 | def white(str):
33 | return Fore.WHITE + str + Style.RESET_ALL
34 |
35 | @staticmethod
36 | def black(str):
37 | return Fore.BLACK + str + Style.RESET_ALL
38 |
39 | @staticmethod
40 | def light_red(str):
41 | return Fore.LIGHTRED_EX + str + Style.RESET_ALL
42 |
43 | @staticmethod
44 | def light_green(str):
45 | return Fore.LIGHTGREEN_EX + str + Style.RESET_ALL
46 |
47 | @staticmethod
48 | def light_yellow(str):
49 | return Fore.LIGHTYELLOW_EX + str + Style.RESET_ALL
50 |
51 | @staticmethod
52 | def light_blue(str):
53 | return Fore.LIGHTBLUE_EX + str + Style.RESET_ALL
54 |
55 | @staticmethod
56 | def light_magenta(str):
57 | return Fore.LIGHTMAGENTA_EX + str + Style.RESET_ALL
58 |
59 |
60 | def banner():
61 | print(r"""
62 |
63 | ____ ____ __ ____ ____ ___
64 | / __// /\ \/ //_ / / __// _ \
65 | / _/ / /__\ / / /_ / _/ / , _/
66 | /___//____//_/ /___//___//_/|_| v0.5.0
67 |
68 | """+
69 | (Colors.light_blue("\n\tAuthor: B0lg0r0v") + Colors.light_blue("\n\thttps://arthurminasyan.com\n")))
70 |
71 |
--------------------------------------------------------------------------------
/core/exp_json.py:
--------------------------------------------------------------------------------
1 | import re
2 | import os
3 | import json
4 | from core.utils import Utils
5 | from datetime import datetime
6 | from core.spoofing import Spoofing
7 |
8 | def parse_key_value(text):
9 | result = {}
10 | for line in text.split('\n'):
11 | line = line.strip()
12 | if ':' in line:
13 | key, value = line.split(':', 1)
14 | result[key.strip()] = value.strip()
15 | return result
16 |
17 | def export_to_json(results, filename=None):
18 | if filename is None:
19 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
20 | filename = f"email_analysis_{timestamp}.json"
21 |
22 | # Ensure the directory exists
23 | os.makedirs(os.path.dirname(filename), exist_ok=True)
24 |
25 | with open(filename, 'w', encoding='utf-8') as f:
26 | json.dump(results, f, ensure_ascii=False, indent=4)
27 |
28 | return filename
29 |
30 | class ExportJson:
31 |
32 | def __init__(self, file):
33 | self.utils = Utils(file)
34 | self.spoofing = Spoofing(file)
35 |
36 |
37 | def jsonoutput(self):
38 | return {
39 | "general_information": self.parse_general_information(),
40 | "security_information": self.parse_security_information(),
41 | "routing": self.parse_routing(),
42 | "envelope": self.parse_envelope(),
43 | "spoofing_check": self.parse_spoofing_check(),
44 | }
45 |
46 |
47 | def parse_general_information(self):
48 | return parse_key_value(self.utils.generalInformation())
49 |
50 | def parse_security_information(self):
51 |
52 | security_info = parse_key_value(self.utils.securityInformations())
53 |
54 | # Parse DKIM signature
55 | if 'DKIM Signature' in security_info:
56 | dkim = {}
57 | for part in security_info['DKIM Signature'].split(';'):
58 | if '=' in part:
59 | key, value = part.split('=', 1)
60 | dkim[key.strip()] = value.strip()
61 | security_info['DKIM Signature'] = dkim
62 |
63 | return security_info
64 |
65 | def parse_routing(self):
66 | routing_info = self.utils.routing().split('\n\n')
67 | hops = []
68 | for hop in re.findall(r'Hop \d+ \|↓\|: (.*)', routing_info[0]):
69 | hop_parts = hop.split(' TO ')
70 | from_part = hop_parts[0].split('FROM ')[1]
71 | to_part, with_part = hop_parts[1].split(' WITH ')
72 | hops.append({
73 | 'from': from_part,
74 | 'to': to_part,
75 | 'with': with_part
76 | })
77 |
78 | timestamps = []
79 | for timestamp in re.findall(r'Hop \d+: (.*)', routing_info[1]):
80 | ts_parts = timestamp.split(', Delta: ')
81 | timestamps.append({
82 | 'timestamp': ts_parts[0],
83 | 'delta': ts_parts[1] if len(ts_parts) > 1 else None
84 | })
85 |
86 | return {
87 | 'hops': [dict(hop, **ts) for hop, ts in zip(hops, timestamps)]
88 | }
89 |
90 | def parse_envelope(self):
91 | envelope_info = parse_key_value(self.utils.envelope())
92 |
93 | # Parse MS Exchange Organization Headers
94 | if 'MS Exchange Organization Headers' in envelope_info:
95 | ms_headers = parse_key_value(envelope_info['MS Exchange Organization Headers'])
96 | envelope_info['MS Exchange Organization'] = ms_headers
97 | del envelope_info['MS Exchange Organization Headers']
98 |
99 | return envelope_info
100 |
101 |
102 | def parse_spoofing_check(self):
103 | spoofing_info = self.spoofing.spoofing_all_checks().split('\n\n')
104 | result = {
105 | 'smtp_server_mismatch': {},
106 | 'field_mismatches': {},
107 | 'external_checks': {}
108 | }
109 |
110 | for section in spoofing_info:
111 | if section.startswith('Checking for SMTP Server Mismatch'):
112 | result['smtp_server_mismatch'] = {'details': section.split('...\n')[1].strip()}
113 | elif section.startswith('Checking for Field Mismatches'):
114 | for line in section.split('\n')[1:]:
115 | if '→' in line:
116 | key, value = line.split('→')
117 | result['field_mismatches'][key.strip()] = value.strip()
118 | elif section.startswith('Checking with VirusTotal'):
119 | result['external_checks']['virustotal'] = parse_key_value(section)
120 | elif section.startswith('Checking with AbuseIPDB'):
121 | result['external_checks']['abuseipdb'] = parse_key_value(section)
122 | elif section.startswith('Checking with IPQualityScore'):
123 | result['external_checks']['ipqualityscore'] = parse_key_value(section)
124 |
125 | return result
126 |
127 | def parse_spoofing_no_dns(self):
128 | spoofing_info = self.spoofing.spoofing_no_dns().split('\n\n')
129 | result = {
130 | 'smtp_server_mismatch': {},
131 | 'field_mismatches': {},
132 | 'external_checks': {}
133 | }
134 |
135 | for section in spoofing_info:
136 | if section.startswith('Checking for SMTP Server Mismatch'):
137 | result['smtp_server_mismatch'] = {'details': section.split('...\n')[1].strip()}
138 | elif section.startswith('Checking for Field Mismatches'):
139 | for line in section.split('\n')[1:]:
140 | if '→' in line:
141 | key, value = line.split('→')
142 | result['field_mismatches'][key.strip()] = value.strip()
143 | elif section.startswith('Checking with VirusTotal'):
144 | result['external_checks']['virustotal'] = parse_key_value(section)
145 | elif section.startswith('Checking with AbuseIPDB'):
146 | result['external_checks']['abuseipdb'] = parse_key_value(section)
147 | elif section.startswith('Checking with IPQualityScore'):
148 | result['external_checks']['ipqualityscore'] = parse_key_value(section)
149 |
150 | return result
151 |
--------------------------------------------------------------------------------
/core/spoofing.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import sys
4 | import requests
5 | import ipaddress
6 | import dns.resolver
7 | from core.utils import Utils
8 | from core.colors import Colors
9 | from email.parser import BytesParser
10 |
11 | class Spoofing:
12 |
13 | def __init__(self, header):
14 | self.eHeader = header
15 | self.colors = Colors()
16 | self.utils = Utils(self.eHeader)
17 | self.resolveIP = self.utils.resolveIP
18 | self.indent = " "
19 | self.resolver = dns.resolver.Resolver()
20 | if 'DRIFTNET_API' not in os.environ:
21 | self.api_key_driftnet = None
22 | else:
23 | self.api_key_driftnet = os.environ['DRIFTNET_API'] # Get your API key here: https://driftnet.io
24 |
25 | #--------------------- This is the "all checks" function. It performs actively DNS resolution. ---------------------#
26 |
27 | def spoofing_all_checks(self):
28 |
29 | report = []
30 |
31 | try:
32 | with open(self.eHeader, 'r', encoding='UTF-8') as header:
33 | content = BytesParser().parsebytes(header.read().encode('UTF-8'))
34 | except FileNotFoundError:
35 | #print(f'{Fore.RED}File not found.{Fore.RESET}')
36 | self.colors.red("File not found.")
37 | sys.exit(1)
38 |
39 | #print(f'\n{Fore.LIGHTBLUE_EX}Spoofing Check: {Fore.RESET}')
40 | print(self.colors.light_blue("\nSpoofing Check:"))
41 | report.append(f'\nSpoofing Check:\n')
42 |
43 | #------------------------Regex and Field definitions------------------------#
44 |
45 | x = next(iter(reversed(self.utils.getReceivedFields())), None)
46 | if x is not None:
47 | ipv4 = re.findall(r'[\[\(](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[\]\)]', x, re.IGNORECASE)
48 | #ipv6 = re.findall(r'[\[\(]([A-Fa-f0-9:]+)[\]\)]', x, re.IGNORECASE)
49 | else:
50 | ipv4 = []
51 | print(self.colors.red("No 'Received' fields found in the email header."))
52 |
53 | # get rid of the localhost IP Address
54 | filteredIpv4 = [ip for ip in ipv4 if ip != '127.0.0.1']
55 |
56 | formatReturnPath = False
57 | formatReplyTo = False
58 |
59 |
60 | fromMatch = re.search(r'<(.*)>', content['from'])
61 | if content['return-path'] is not None:
62 | if '<' in content['return-path']:
63 | returnToPath = re.search(r'<(.*?)>', content['return-path'])
64 | formatReturnPath = True
65 | if content['reply-to'] is not None:
66 | if '<' in content['reply-to']:
67 | replyTo = re.search(r'<(.*)>', content['reply-to'])
68 | formatReplyTo = True
69 |
70 |
71 | #------------------------check for spoofing------------------------#
72 | mx = []
73 | aRecordsOfMx = []
74 |
75 | mxAuthResult = []
76 | aRecordsOfMxAuthResult = []
77 | authResultOrigIP = None
78 |
79 | # Getting the Domain Name from the "From" Field
80 | if fromMatch is not None:
81 | fromEmailDomain = fromMatch.group(1).split('@')[1]
82 | else:
83 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
84 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n')
85 |
86 | # Getting the MX Records from the Domain Name
87 | try:
88 | getMx = self.resolver.resolve(fromEmailDomain, 'MX')
89 | for servers in getMx:
90 | mx.append(servers.exchange.to_text().lower())
91 |
92 | except (dns.resolver.LifetimeTimeout, dns.resolver.NoAnswer,dns.resolver.NXDOMAIN,dns.resolver.YXDOMAIN,dns.resolver.NoNameservers):
93 | print(self.colors.red("Could not resolve the MX Record."))
94 | # Resolving the A Records from the MX Records
95 | for servers in mx:
96 | aRecordsOfMx.append(self.resolveIP(servers))
97 |
98 |
99 | print(self.colors.magenta("\nChecking for SMTP Server Mismatch..."))
100 | report.append('\nChecking for SMTP Server Mismatch...\n')
101 |
102 | if filteredIpv4:
103 | if filteredIpv4[0] in aRecordsOfMx:
104 | #print(f'{Fore.LIGHTGREEN_EX}No Mismatch detected.{Fore.RESET}')
105 | print(self.colors.green("No Mismatch detected."))
106 | report.append(f'No Mismatch detected.')
107 | else:
108 | print(self.colors.yellow(f'{self.indent}→ Potential SMTP Server Mismatch detected. Sender SMTP Server is "{fromEmailDomain} [{"".join(filteredIpv4[0])}]" and should be "{fromEmailDomain} [{", ".join(aRecordsOfMx)}]" <- (current MX Record(s) for this domain.)'))
109 | report.append(f'{self.indent}→ Potential SMTP Server Mismatch detected. Sender SMTP Server is "{fromEmailDomain} [{"".join(filteredIpv4[0])}]" and should be "{fromEmailDomain} [{", ".join(aRecordsOfMx)}]" <- (current MX Record(s) for this domain.)')
110 | else:
111 |
112 | #------------------------ If we get no IP addresses from the received from field, we will try to get the IP Address from the "Authentication-Results-Original" Field and do diverse lookups with it. ------------------------#
113 |
114 | # Get the content of the "Authentication-Results-Origin" Field and extract the IPv4 Address which is always after the string "sender IP is "
115 | if isinstance(content['Authentication-Results-Original'], str):
116 |
117 | authResultsOrigin = re.findall(r'sender IP is ([\d.]+)', content['Authentication-Results-Original'], re.IGNORECASE)
118 | if authResultsOrigin:
119 | ipv4 = authResultsOrigin
120 | authResultOrigIP = [ip for ip in ipv4 if ip != '127.0.0.1']
121 | #print(''.join(authResultIP))
122 |
123 | # doing a reverse lookup for the authResultOrigIP and getting the domain name
124 | try:
125 | authResultOrigDomain = self.resolver.resolve(dns.reversename.from_address(''.join(authResultOrigIP)), 'PTR')
126 | for domain in authResultOrigDomain:
127 | authResultOrig = domain.to_text().lower()
128 | #print(authResultDomain)
129 |
130 | except (dns.resolver.LifetimeTimeout, dns.resolver.NoAnswer):
131 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the Domain Name.{Fore.RESET}')
132 | print(self.colors.red("Could not resolve the Domain Name."))
133 | report.append(f'Could not resolve the Domain Name.')
134 |
135 | # Remove the subdomain from the domain name
136 | tmp = authResultOrig.split('.')
137 | authResultFullDomain = '.'.join(tmp[-3:-1])
138 |
139 | # Get the MX Records of authResultFullDomain
140 | try:
141 | authResultMx = self.resolver.resolve(authResultFullDomain, 'MX')
142 | for servers in authResultMx:
143 | mxAuthResult.append(servers.exchange.to_text().lower())
144 |
145 | except dns.resolver.LifetimeTimeout:
146 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the MX Record.{Fore.RESET}')
147 | print(self.colors.red("Could not resolve the MX Record."))
148 | report.append(f'Could not resolve the MX Record.')
149 |
150 |
151 | # Resolving the A Records from mxAuthResult
152 | for n in mxAuthResult:
153 | aRecordsOfMxAuthResult.append(self.resolveIP(n))
154 |
155 | #print(aRecordsOfMxAuthResult)
156 | #print(aRecordsOfMx, mxAuthResult)
157 | # If one of the values of mxAuthResults is in the mx list, then there is no spoofing.
158 | if any(x in aRecordsOfMxAuthResult for x in aRecordsOfMx):
159 | #print(f'{Fore.LIGHTGREEN_EX}No Mismatch detected.{Fore.RESET}')
160 | print(self.colors.green("No Mismatch detected."))
161 | report.append(f'No Mismatch detected.')
162 | #print(aRecordsOfMxAuthResult[0])
163 |
164 | else:
165 | print(self.colors.yellow(f'{self.indent}No IPv4 Address detected in "FROM" Field. Doing additional checks...'))
166 | #report.append(f'{indent}No IPv4 Address detected in "FROM" Field. Doing additional checks...')
167 | report.append(f'{self.indent}No IPv4 Address detected in "FROM" Field. Doing additional checks...')
168 | # If there is no value matching, then we need to retrieve the spf records of mxAuthResults. Extract the values between the quotes.
169 | txtRecords = []
170 | try:
171 | authResultSpf = self.resolver.resolve(authResultFullDomain, 'TXT')
172 | for spf in authResultSpf:
173 | authResultSpf = spf.to_text().lower()
174 | txtRecords.append(re.findall(r'"(.*?)"', authResultSpf))
175 |
176 | except dns.resolver.LifetimeTimeout:
177 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the SPF Record.{Fore.RESET}')
178 | print(self.colors.red("Could not resolve the SPF Record."))
179 | report.append(f'{self.indent}Could not resolve the SPF Record.')
180 |
181 |
182 | # get the subnets from the txtRecords
183 | subnetsTmp = []
184 | for txt in txtRecords:
185 | for subnet in txt:
186 | # Extract the subnets from the txtRecords
187 | subnetsTmp.append(re.findall(r'ip4:(.*)', subnet))
188 |
189 |
190 | # if the list subnetsTmp does not contain something with "ip4" (most of cases), then we can continue with the check. If it is empty, then we need to do a lookup with the "include" values from the txtRecords.
191 | if any('ip4:' in subnetss for sublist in subnetsTmp for subnetss in sublist):
192 | # extract the subnets from the list and put them in a new list
193 | subnets = []
194 | for subnet in subnetsTmp:
195 | for x in subnet:
196 | substrings = x.split(' ')
197 | subnets.extend(s for s in substrings if s.startswith('ip4:'))
198 |
199 |
200 | # convert the subnets to ipaddress objects
201 | ipSubnets = []
202 | for subnet in subnets:
203 | subnet = subnet.replace('ip4:', '') # Remove 'ip4:' prefix
204 | networks = subnet.split(' ') # Split the string into individual networks
205 | for network in networks:
206 | if '/' in network: # Ignore non-IP strings like '-all'
207 | ipSubnets.append(ipaddress.ip_network(network, strict=False))
208 |
209 | if any(ipaddress.ip_address(authResultOrigIP[0]) in subnet for subnet in ipSubnets):
210 | print(self.colors.green(f'{self.indent}{self.indent}→ No Mismatch detected.'))
211 | report.append(f'\n{self.indent}{self.indent}→ No Mismatch detected.')
212 | else:
213 | print(self.colors.light_yellow(f'{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})'))
214 | report.append(f'\n{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})')
215 |
216 |
217 | elif not any(subnet for subnet in subnetsTmp): #if subnetTmp is empty, then we can try to get the "include" values from the txtRecords and do a lookup with them.
218 | includeTmp = []
219 | for txt in txtRecords:
220 | for include in txt:
221 | includeTmp.append(re.findall(r'include:(.*)', include))
222 |
223 | includeTmp = [x for x in includeTmp if x]
224 |
225 | # Extract value of includeTmp. This shit hurts.
226 | extractionIncludeValue = [x for sublist in includeTmp for x in sublist]
227 |
228 | extractionIncludeValue = [mechanism for string in extractionIncludeValue for mechanism in string.split()] #split this shit
229 | extractionIncludeValue = [mechanism for mechanism in extractionIncludeValue if mechanism not in ["~all", "-all"]] # Remove "~all" and "-all" from extractionIncludeValue
230 |
231 | txtRecordsOfInclude = []
232 | for include in extractionIncludeValue:
233 | try:
234 | spfResultsInclude = self.resolver.resolve(include, 'TXT')
235 | for spfInclude in spfResultsInclude:
236 | spfResultsInclude = spfInclude.to_text().lower()
237 | txtRecordsOfInclude.append(re.findall(r'"(.*?)"', spfResultsInclude))
238 | except dns.resolver.LifetimeTimeout:
239 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the SPF Record.{Fore.RESET}')
240 | print(self.colors.red("Could not resolve the SPF Record."))
241 | report.append('Could not resolve the SPF Record.')
242 |
243 | # Now this shit gets funny
244 | extractionSecondLevel = [y for subsublist in txtRecordsOfInclude for y in subsublist]
245 |
246 | extractionSecondLevel = [technique for idontknow in extractionSecondLevel for technique in idontknow.split()]
247 | extractionSecondLevel = [technique for technique in extractionSecondLevel if technique not in['~all', '-all']]
248 | extractionSecondLevel = [technique for technique in extractionSecondLevel if technique != 'v=spf1']
249 |
250 | # Extract the domain from the include mechanism
251 | includeDomains = [mechanism.split(':')[1] for mechanism in extractionSecondLevel if mechanism.startswith('include:')]
252 |
253 | txtRecordOfIncludeSecond = []
254 | for domain in includeDomains:
255 | try:
256 | resultsOfInclude = self.resolver.resolve(domain, 'TXT')
257 | for p in resultsOfInclude:
258 | includeResults = p.to_text().lower()
259 | txtRecordOfIncludeSecond.append(re.findall(r'"(.*?)"', includeResults))
260 | except dns.resolver.LifetimeTimeout:
261 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the SPF Record.{Fore.RESET}')
262 | print(self.colors.red("Could not resolve the SPF Record."))
263 | report.append('Could not resolve the SPF Record.')
264 |
265 |
266 | subnetsOfInclude = []
267 | for b in txtRecordOfIncludeSecond:
268 | for h in b:
269 | subnetsOfInclude.append(re.findall(r'ip4:(.*)', h))
270 |
271 | if any('ip4:' in subnet for sublist in subnetsOfInclude for subnet in sublist):
272 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}{indent}Getting deeper into the SPF Records...{Fore.RESET}')
273 | print(self.colors.yellow(f'{self.indent}{self.indent}Getting deeper into the SPF Records...'))
274 | report.append(f'\n{self.indent}{self.indent}Getting deeper into the SPF Records...')
275 | # extract the subnets from the list and put them in a new list
276 | subnets = []
277 | for subnet in subnetsOfInclude:
278 | for x in subnet:
279 | substrings = x.split(' ')
280 | subnets.extend(s for s in substrings if s.startswith('ip4:'))
281 |
282 | # convert the subnets to ipaddress objects
283 | ipSubnets = []
284 | for subnet in subnets:
285 | subnet = subnet.replace('ip4:', '') # Remove 'ip4:' prefix
286 | networks = subnet.split(' ')
287 | for network in networks:
288 | if '/' in network:
289 | ipSubnets.append(ipaddress.ip_network(network, strict=False))
290 |
291 | if any(ipaddress.ip_address(authResultOrigIP[0]) in subnet for subnet in ipSubnets):
292 | #print(f'{Fore.LIGHTGREEN_EX}{indent}{indent}→ No Mismatch detected.{Fore.RESET}')
293 | print(self.colors.green(f'{self.indent}{self.indent}→ No Mismatch detected.'))
294 | report.append(f'\n{self.indent}{self.indent}→ No Mismatch detected.')
295 | else:
296 | #print(f'{Fore.LIGHTRED_EX}{indent}{indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets}){Fore.RESET}')
297 | print(self.colors.red(f'{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})'))
298 | report.append(f'\n{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})')
299 |
300 | else:
301 | #print(f'{Fore.LIGHTRED_EX}{indent}{indent}→ Could not detect SPF Record. Manual Reviewing required.{Fore.RESET}')
302 | print(self.colors.red(f'{self.indent}{self.indent}→ Could not detect SPF Record. Manual Reviewing required.'))
303 | report.append(f'\n{self.indent}{self.indent}→ Could not detect SPF Record. Manual Reviewing required.')
304 |
305 | else:
306 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}')
307 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
308 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n')
309 |
310 | #------------------------Check for Field Mismatches------------------------#
311 |
312 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking for Field Mismatches...{Fore.RESET}')
313 | print(self.colors.magenta("\nChecking for Field Mismatches..."))
314 | report.append('\nChecking for Field Mismatches...\n')
315 |
316 | if content['message-id'] is not None:
317 | #print(f'{Fore.LIGHTGREEN_EX}Message-ID Field detected !{Fore.RESET}')
318 | print(self.colors.green("Message-ID Field detected !"))
319 | report.append('Message-ID Field detected !\n')
320 | # Get the domain name between the "<>" brackets and split it at the "@" sign
321 | messageIDDomain = content['message-id'].split('@')[1].split('>')[0]
322 | #print(messageIDDomain)
323 | if fromEmailDomain.strip().lower() != messageIDDomain.strip().lower():
324 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"{Fore.RESET}')
325 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"'))
326 | report.append(f'{self.indent}→ Suspicious activity detected: Message-ID Domain ({messageIDDomain}) NOT EQUAL "FROM" Domain ({fromEmailDomain})\n')
327 | else:
328 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No Mismatch detected.{Fore.RESET}')
329 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.'))
330 | report.append(f'{self.indent}→ No Mismatch detected.\n')
331 |
332 | else:
333 | #print(f'{Fore.WHITE}No Message-ID Field detected. Skipping...{Fore.RESET}')
334 | print(self.colors.white("No Message-ID Field detected. Skipping..."))
335 | report.append('No Message-ID Field detected. Skipping...\n')
336 |
337 |
338 | if fromMatch.group(1) is not None and content['reply-to'] is not None:
339 |
340 | #print(f'{Fore.LIGHTGREEN_EX}Reply-To Field detected !{Fore.RESET}')
341 | print(self.colors.green("Reply-To Field detected !"))
342 | report.append('Reply-To Field detected !')
343 |
344 | if formatReplyTo == False:
345 | if content['from'].strip().lower() != content['reply-to'].strip().lower():
346 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]}){Fore.RESET}')
347 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})'))
348 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})')
349 |
350 | else:
351 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}')
352 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.'))
353 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')
354 |
355 | elif formatReplyTo == True:
356 | if fromMatch.group(1).strip().lower() != replyTo.group(1).strip().lower():
357 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)}){Fore.RESET}')
358 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})'))
359 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})')
360 |
361 | else:
362 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}')
363 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.'))
364 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')
365 |
366 | else:
367 | #print(f'{Fore.WHITE}No Reply-To Field detected. Skipping...{Fore.RESET}')
368 | print(self.colors.white("No Reply-To Field detected. Skipping..."))
369 | report.append('No Reply-To Field detected. Skipping...\n')
370 |
371 | if fromMatch.group(1) is not None and content['return-path'] is not None:
372 |
373 | #print(f'{Fore.LIGHTGREEN_EX}Return-Path Field detected !{Fore.RESET}')
374 | print(self.colors.green("Return-Path Field detected !"))
375 | report.append('\nReturn-Path Field detected !')
376 |
377 | if formatReturnPath == False:
378 | if fromMatch.group(1).strip().lower() != content['return-path'].strip().lower():
379 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]}){Fore.RESET}')
380 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})'))
381 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})')
382 |
383 | else:
384 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.'))
385 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')
386 |
387 | elif formatReturnPath == True:
388 | if fromMatch.group(1).strip().lower() != returnToPath.group(1).strip().lower():
389 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})'))
390 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})')
391 |
392 | else:
393 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.'))
394 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')
395 |
396 | else:
397 | print(self.colors.white("No Return-Path Field detected. Skipping..."))
398 | report.append('No Return-Path Field detected. Skipping...')
399 |
400 | #------------------------Check with VirusTotal------------------------#
401 |
402 | #print(f'\n{Fore.LIGHTYELLOW_EX}Note: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.{Fore.RESET}')
403 | print(self.colors.yellow("\nNote: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code."))
404 |
405 | print(self.colors.magenta("\nChecking with VirusTotal..."))
406 | report.append('\n\nChecking with VirusTotal...\n')
407 |
408 | if filteredIpv4:
409 | # If you got an VT API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
410 | #os.system(f'curl -s -X GET --header "x-apikey: " "https://www.virustotal.com/api/v3/ip_addresses/{ipv4[0]}" > vt.json')
411 |
412 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection")}')
413 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations")}')
414 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph")}')
415 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic")}')
416 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois")}')
417 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments")}')
418 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes")}')
419 |
420 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection\n')
421 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations\n')
422 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph\n')
423 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic\n')
424 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois\n')
425 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments\n')
426 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes\n')
427 |
428 |
429 | elif authResultOrigIP:
430 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection")}'),
431 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations")}')
432 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph")}')
433 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic")}')
434 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois")}')
435 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments")}')
436 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes")}')
437 |
438 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection\n')
439 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations\n')
440 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph\n')
441 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic\n')
442 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois\n')
443 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments\n')
444 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes\n')
445 |
446 |
447 |
448 | else:
449 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}')
450 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
451 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
452 |
453 | #------------------------Check with AbuseIPDB------------------------#
454 |
455 | print(self.colors.magenta("\nChecking with AbuseIPDB..."))
456 | report.append('\nChecking with AbuseIPDB...\n')
457 |
458 | if filteredIpv4:
459 | # If you got an AbuseIPDB API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
460 | #os.system(f'curl -s -X GET --header "Key: " "https://api.abuseipdb.com/api/v2/check?ipAddress={ipv4[0]}" > abuseipdb.json')
461 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{filteredIpv4[0]}")}')
462 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{filteredIpv4[0]}')
463 |
464 | elif authResultOrigIP:
465 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{authResultOrigIP[0]}")}')
466 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{authResultOrigIP[0]}')
467 |
468 | else:
469 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
470 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
471 |
472 | #------------------------Check with IPQualityScore------------------------#
473 |
474 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking with IPQualityScore...{Fore.RESET}')
475 | print(self.colors.magenta("\nChecking with IPQualityScore..."))
476 | report.append('\n\nChecking with IPQualityScore...\n')
477 |
478 | if filteredIpv4:
479 | # If you got an IPQualityScore API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
480 | #os.system(f'curl -s -X GET --header "Key: " "https://www.ipqualityscore.com/api/json/ip//{ipv4[0]}" > ipqualityscore.json')
481 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}{Fore.RESET}')
482 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}")}')
483 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}')
484 |
485 | elif authResultOrigIP:
486 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}{Fore.RESET}')
487 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}")}')
488 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}')
489 |
490 | else:
491 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}')
492 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
493 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
494 |
495 | return ''.join(report)
496 |
497 |
498 | #------------------------This part is for the passive DNS check.------------------------#
499 |
500 | # Function for fetching A Records via the Driftnet API
501 | def passive_a_records_driftnet(self, mx_server) -> list:
502 |
503 | a_records = []
504 | url = f"https://api.driftnet.io/v1/domain/fdns?host={mx_server}"
505 |
506 | headers = {
507 | 'Authorization': f'Bearer {self.api_key_driftnet}',
508 | 'Content-Type': 'application/json'
509 | }
510 |
511 | response = requests.get(url, headers=headers)
512 | if response.status_code == 200:
513 |
514 | parsed_json = response.json()
515 |
516 | # First iteration is for parsing the JSON structure first
517 | for x in parsed_json['results']:
518 | # Second iteration is for getting the 'items' key
519 | for y in x['items']:
520 | # Third iteration is for filtering the results and getting the exact host value that matches the mx_server variable
521 | if y['value'] == mx_server:
522 | # Fourth iteration is iterating again over the 'items' key, but this time we have filtered it and we can get the context key that equals to 'dns-a
523 | for z in x['items']:
524 | if z['context'] == 'dns-a':
525 | a_records.append(z['value'])
526 |
527 | return a_records
528 |
529 |
530 | else:
531 | #print(self.colors.red(f"[DEBUG] Error getting A records for {mx_server}: {response.status_code}"))
532 | return None
533 |
534 | # Funtion for fetching MX Records via the Driftnet API
535 | def passive_mx_records_driftnet(self, domain) -> list:
536 |
537 | results = []
538 |
539 | try:
540 | url = f"https://api.driftnet.io/v1/domain/mx?host={domain}"
541 |
542 | headers = {
543 | 'Authorization': f'Bearer {self.api_key_driftnet}',
544 | 'Content-Type': 'application/json'
545 | }
546 |
547 | # Doing the actual request
548 | response = requests.get(url, headers=headers)
549 | if response.status_code == 200:
550 |
551 | parsed_json = response.json()
552 |
553 | for x in parsed_json['results']:
554 | for y in x['items']:
555 | if y['type'] == 'host':
556 | for z in x['items']:
557 | if z['context'] == 'dns-mx' and z['type'] == 'host':
558 | results.append(z['value'])
559 |
560 |
561 | # filtering out the duplicates from results
562 | return list(set(results))
563 |
564 | except Exception as e:
565 | print(self.colors.red(f"Error getting MX records for {domain}: {str(e)}"))
566 | return[]
567 |
568 |
569 | # Revere DNS lookup via the Driftnet API
570 | def passive_reverse_dns_driftnet(self, ip) -> list:
571 | url = f"https://api.driftnet.io/v1/domain/rdns?ip={ip}"
572 |
573 | headers = {
574 | 'Authorization': f'Bearer {self.api_key_driftnet}',
575 | 'Content-Type': 'application/json'
576 | }
577 |
578 | try:
579 | response = requests.get(url, headers=headers)
580 | if response.status_code == 200:
581 | parsed_data = self.parse_driftnet_response(response.json())
582 | return parsed_data['ptr_records']
583 | else:
584 | print(self.colors.red(f"Error getting reverse DNS for {ip}: {response.status_code}"))
585 | return []
586 | except Exception as e:
587 | print(self.colors.red(f"Error getting reverse DNS for {ip}: {str(e)}"))
588 | return []
589 |
590 |
591 | @staticmethod
592 | def parse_driftnet_response(data) -> dict:
593 | try:
594 | values = []
595 | ptr_records = []
596 |
597 | for result in data.get('results', []):
598 | for item in result.get('items', []):
599 | value = item.get('value')
600 | if value:
601 | values.append(value)
602 | if item.get('context') == 'dns-ptr':
603 | ptr_records.append(value)
604 |
605 | return {
606 | 'all_values': values,
607 | 'ptr_records': ptr_records
608 | }
609 |
610 | except KeyError as e:
611 | print(f"Error: Missing expected key in JSON structure: {e}")
612 | return {'all_values': [], 'ptr_records': []}
613 |
614 |
615 | def spoofing_passive_dns(self):
616 |
617 | report = []
618 |
619 | try:
620 | with open(self.eHeader, 'r', encoding='UTF-8') as header:
621 | content = BytesParser().parsebytes(header.read().encode('UTF-8'))
622 | except FileNotFoundError:
623 | self.colors.red("File not found.")
624 | sys.exit(1)
625 |
626 | print(self.colors.light_blue("\nSpoofing Check:"))
627 | report.append(f'\nSpoofing Check:\n')
628 |
629 | #------------------------Regex and Field definitions------------------------#
630 |
631 | x = next(iter(reversed(self.utils.getReceivedFields())), None)
632 | if x is not None:
633 | ipv4 = re.findall(r'[\[\(](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[\]\)]', x, re.IGNORECASE)
634 | #ipv6 = re.findall(r'[\[\(]([A-Fa-f0-9:]+)[\]\)]', x, re.IGNORECASE)
635 | else:
636 | ipv4 = []
637 | print(self.colors.red("No 'Received' fields found in the email header."))
638 |
639 | # get rid of the localhost IP Address
640 | filteredIpv4 = [ip for ip in ipv4 if ip != '127.0.0.1']
641 |
642 | formatReturnPath = False
643 | formatReplyTo = False
644 |
645 |
646 | fromMatch = re.search(r'<(.*)>', content['from'])
647 | if content['return-path'] is not None:
648 | if '<' in content['return-path']:
649 | returnToPath = re.search(r'<(.*?)>', content['return-path'])
650 | formatReturnPath = True
651 | if content['reply-to'] is not None:
652 | if '<' in content['reply-to']:
653 | replyTo = re.search(r'<(.*)>', content['reply-to'])
654 | formatReplyTo = True
655 |
656 |
657 | #------------------------check for spoofing------------------------#
658 | aRecordsOfMxAuthResult = []
659 | authResultOrigIP = None
660 |
661 | # Getting the Domain Name from the "From" Field
662 | if fromMatch is not None:
663 | fromEmailDomain = fromMatch.group(1).split('@')[1]
664 | else:
665 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
666 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n')
667 |
668 |
669 | #print('Email Domain:', fromEmailDomain) # Debug statement
670 |
671 | DF_MX_A_RECORD = self.passive_a_records_driftnet(fromEmailDomain)
672 | if DF_MX_A_RECORD is None:
673 | print(self.colors.red("Could not retrieve data from Driftnet."))
674 | return ''.join(report)
675 |
676 | #print('A Records:', DF_MX_A_RECORD) # Debug statement
677 | DF_MX_A_RECORD = list(dict.fromkeys(DF_MX_A_RECORD))
678 |
679 | print(self.colors.light_magenta("\nChecking for SMTP Server Mismatch..."))
680 | report.append(f'\nChecking for SMTP Server Mismatch...\n')
681 |
682 | if filteredIpv4:
683 | for tmp in DF_MX_A_RECORD:
684 | if filteredIpv4[0] in tmp:
685 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.'))
686 | report.append(f'{self.indent}→ No Mismatch detected.')
687 | else:
688 | print(self.colors.yellow(f'{self.indent}→ Potential SMTP Server Mismatch detected. Sender SMTP Server is {fromEmailDomain} [{filteredIpv4[0]}] and should be {tmp} <- (current MX Record(s) for this domain)'))
689 | report.append(f'{self.indent}→ Suspicious activity detected: SMTP Server Mismatch detected.')
690 |
691 | else:
692 | if isinstance(content['Authentication-Results-Original'], str):
693 | authResultsOrigin = re.findall(r'sender IP is ([\d.]+)', content['Authentication-Results-Original'], re.IGNORECASE)
694 | if authResultsOrigin:
695 | ipv4 = authResultsOrigin
696 | authResultOrigIP = [ip for ip in ipv4 if ip != '127.0.0.1']
697 |
698 | else:
699 | print(self.colors.red("No 'Authentication-Results-Original' Header found. Manual reviewing required."))
700 |
701 | #print(authResultOrigIP) # Debug statement
702 | if authResultOrigIP:
703 | try:
704 | for domain in authResultOrigIP:
705 | associated_domain = self.passive_reverse_dns_driftnet(domain)
706 | # filtering duplicates
707 | associated_domain = list(dict.fromkeys(associated_domain))
708 | #print(associated_domain) # Debug statement
709 |
710 |
711 | for subdomain in associated_domain:
712 | tmp = subdomain.split('.')
713 | authResultFullDomain = '.'.join(tmp[-2:])
714 | #print(authResultFullDomain) # Debug statement
715 |
716 | mx_records_from_df = self.passive_mx_records_driftnet(authResultFullDomain)
717 | #print(mx_records_from_df) # Debug statement
718 |
719 | for x in mx_records_from_df:
720 | aRecordsOfMxAuthResult.append(self.passive_a_records_driftnet(x))
721 | #print(aRecordsOfMxAuthResult) # Debug statement
722 |
723 | # Flattening the list
724 | if aRecordsOfMxAuthResult:
725 | aRecordsOfMxAuthResult = [item for sublist in aRecordsOfMxAuthResult if sublist is not None for item in sublist]
726 |
727 | #print(aRecordsOfMxAuthResult) # Debug statement
728 | if any(x in aRecordsOfMxAuthResult for x in DF_MX_A_RECORD):
729 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.'))
730 | report.append(f'{self.indent}→ No Mismatch detected.')
731 |
732 | else:
733 | print(self.colors.light_yellow(f'{self.indent}Could not compare values. Manual reviewing required...'))
734 | report.append(f'{self.indent}Could not compare values. Manual reviewing required...')
735 |
736 | except Exception as e:
737 | print(self.colors.red(f"An error occurred while querying the API: {str(e)}"))
738 |
739 | else:
740 | pass
741 |
742 | #------------------------Check for Field Mismatches------------------------#
743 |
744 | print(self.colors.magenta("\nChecking for Field Mismatches..."))
745 | report.append('\nChecking for Field Mismatches...\n')
746 |
747 | if content['message-id'] is not None:
748 | print(self.colors.green("Message-ID Field detected !"))
749 | report.append('Message-ID Field detected !\n')
750 | # Get the domain name between the "<>" brackets and split it at the "@" sign
751 | messageIDDomain = content['message-id'].split('@')[1].split('>')[0]
752 | #print(messageIDDomain)
753 | if fromEmailDomain.strip().lower() != messageIDDomain.strip().lower():
754 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"'))
755 | report.append(f'{self.indent}→ Suspicious activity detected: Message-ID Domain ({messageIDDomain}) NOT EQUAL "FROM" Domain ({fromEmailDomain})\n')
756 | else:
757 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.'))
758 | report.append(f'{self.indent}→ No Mismatch detected.\n')
759 |
760 | else:
761 | print(self.colors.white("No Message-ID Field detected. Skipping..."))
762 | report.append('No Message-ID Field detected. Skipping...\n')
763 |
764 |
765 | if fromMatch.group(1) is not None and content['reply-to'] is not None:
766 |
767 | print(self.colors.green("Reply-To Field detected !"))
768 | report.append('Reply-To Field detected !')
769 |
770 | if formatReplyTo == False:
771 | if content['from'].strip().lower() != content['reply-to'].strip().lower():
772 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})'))
773 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})')
774 |
775 | else:
776 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.'))
777 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')
778 |
779 | elif formatReplyTo == True:
780 | if fromMatch.group(1).strip().lower() != replyTo.group(1).strip().lower():
781 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})'))
782 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})')
783 |
784 | else:
785 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.'))
786 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')
787 |
788 | else:
789 | print(self.colors.white("No Reply-To Field detected. Skipping..."))
790 | report.append('No Reply-To Field detected. Skipping...\n')
791 |
792 | if fromMatch.group(1) is not None and content['return-path'] is not None:
793 |
794 | print(self.colors.green("Return-Path Field detected !"))
795 | report.append('\nReturn-Path Field detected !')
796 |
797 | if formatReturnPath == False:
798 | if fromMatch.group(1).strip().lower() != content['return-path'].strip().lower():
799 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})'))
800 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})')
801 |
802 | else:
803 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.'))
804 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')
805 |
806 | elif formatReturnPath == True:
807 | if fromMatch.group(1).strip().lower() != returnToPath.group(1).strip().lower():
808 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})'))
809 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})')
810 |
811 | else:
812 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.'))
813 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')
814 |
815 | else:
816 | print(self.colors.white("No Return-Path Field detected. Skipping..."))
817 | report.append('No Return-Path Field detected. Skipping...')
818 |
819 | #------------------------Check with VirusTotal------------------------#
820 |
821 | #print(f'\n{Fore.LIGHTYELLOW_EX}Note: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.{Fore.RESET}')
822 | print(self.colors.yellow("\nNote: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code."))
823 |
824 | print(self.colors.magenta("\nChecking with VirusTotal..."))
825 | report.append('\n\nChecking with VirusTotal...\n')
826 |
827 | if filteredIpv4:
828 | # If you got an VT API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
829 | #os.system(f'curl -s -X GET --header "x-apikey: " "https://www.virustotal.com/api/v3/ip_addresses/{ipv4[0]}" > vt.json')
830 |
831 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection")}')
832 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations")}')
833 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph")}')
834 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic")}')
835 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois")}')
836 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments")}')
837 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes")}')
838 |
839 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection\n')
840 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations\n')
841 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph\n')
842 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic\n')
843 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois\n')
844 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments\n')
845 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes\n')
846 |
847 |
848 | elif authResultOrigIP:
849 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection")}'),
850 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations")}')
851 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph")}')
852 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic")}')
853 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois")}')
854 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments")}')
855 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes")}')
856 |
857 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection\n')
858 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations\n')
859 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph\n')
860 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic\n')
861 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois\n')
862 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments\n')
863 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes\n')
864 |
865 |
866 |
867 | else:
868 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}')
869 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
870 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
871 |
872 | #------------------------Check with AbuseIPDB------------------------#
873 |
874 | print(self.colors.magenta("\nChecking with AbuseIPDB..."))
875 | report.append('\nChecking with AbuseIPDB...\n')
876 |
877 | if filteredIpv4:
878 | # If you got an AbuseIPDB API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
879 | #os.system(f'curl -s -X GET --header "Key: " "https://api.abuseipdb.com/api/v2/check?ipAddress={ipv4[0]}" > abuseipdb.json')
880 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{filteredIpv4[0]}")}')
881 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{filteredIpv4[0]}')
882 |
883 | elif authResultOrigIP:
884 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{authResultOrigIP[0]}")}')
885 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{authResultOrigIP[0]}')
886 |
887 | else:
888 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
889 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
890 |
891 | #------------------------Check with IPQualityScore------------------------#
892 |
893 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking with IPQualityScore...{Fore.RESET}')
894 | print(self.colors.magenta("\nChecking with IPQualityScore..."))
895 | report.append('\n\nChecking with IPQualityScore...\n')
896 |
897 | if filteredIpv4:
898 | # If you got an IPQualityScore API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
899 | #os.system(f'curl -s -X GET --header "Key: " "https://www.ipqualityscore.com/api/json/ip//{ipv4[0]}" > ipqualityscore.json')
900 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}{Fore.RESET}')
901 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}")}')
902 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}')
903 |
904 | elif authResultOrigIP:
905 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}{Fore.RESET}')
906 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}")}')
907 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}')
908 |
909 | else:
910 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}')
911 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
912 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
913 |
914 | return ''.join(report)
915 |
916 |
917 | #------------------------This part is for the NO DNS check.------------------------#
918 |
919 | def spoofing_no_dns(self):
920 |
921 | report = []
922 |
923 | try:
924 | with open(self.eHeader, 'r', encoding='UTF-8') as header:
925 | content = BytesParser().parsebytes(header.read().encode('UTF-8'))
926 | except FileNotFoundError:
927 | self.colors.red("File not found.")
928 | sys.exit(1)
929 |
930 | print(self.colors.light_red("\nNo DNS resolution is performed. This heavily affects the results !"))
931 | report.append(f'No DNS resolution is performed. This heavily affects the results !!\n')
932 |
933 | print(self.colors.light_blue("\nSpoofing Check:"))
934 | report.append(f'\nSpoofing Check:\n')
935 |
936 | #------------------------Regex and Field definitions------------------------#
937 |
938 | x = next(iter(reversed(self.utils.getReceivedFields())), None)
939 | if x is not None:
940 | ipv4 = re.findall(r'[\[\(](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[\]\)]', x, re.IGNORECASE)
941 | ipv6 = re.findall(r'[\[\(]([A-Fa-f0-9:]+)[\]\)]', x, re.IGNORECASE)
942 |
943 | else:
944 | print(self.colors.red("No 'Received' fields found in the email header."))
945 | ipv4 = []
946 | # get rid of the localhost IP Address
947 | filteredIpv4 = [ip for ip in ipv4 if ip != '127.0.0.1']
948 |
949 | formatReturnPath = False
950 | formatReplyTo = False
951 |
952 |
953 | fromMatch = re.search(r'<(.*)>', content['from'])
954 | if content['return-path'] is not None:
955 | if '<' in content['return-path']:
956 | returnToPath = re.search(r'<(.*?)>', content['return-path'])
957 | formatReturnPath = True
958 | if content['reply-to'] is not None:
959 | if '<' in content['reply-to']:
960 | replyTo = re.search(r'<(.*)>', content['reply-to'])
961 | formatReplyTo = True
962 |
963 |
964 | #------------------------check for spoofing------------------------#
965 | authResultOrigIP = None
966 |
967 | # Getting the Domain Name from the "From" Field
968 | if fromMatch is not None:
969 | fromEmailDomain = fromMatch.group(1).split('@')[1]
970 | else:
971 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
972 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n')
973 |
974 |
975 | #------------------------Check for Field Mismatches------------------------#
976 |
977 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking for Field Mismatches...{Fore.RESET}')
978 | print(self.colors.magenta("\nChecking for Field Mismatches..."))
979 | report.append('\nChecking for Field Mismatches...\n')
980 |
981 | if content['message-id'] is not None:
982 | #print(f'{Fore.LIGHTGREEN_EX}Message-ID Field detected !{Fore.RESET}')
983 | print(self.colors.green("Message-ID Field detected !"))
984 | report.append('Message-ID Field detected !\n')
985 | # Get the domain name between the "<>" brackets and split it at the "@" sign
986 | messageIDDomain = content['message-id'].split('@')[1].split('>')[0]
987 | #print(messageIDDomain)
988 | if fromEmailDomain.strip().lower() != messageIDDomain.strip().lower():
989 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"{Fore.RESET}')
990 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"'))
991 | report.append(f'{self.indent}→ Suspicious activity detected: Message-ID Domain ({messageIDDomain}) NOT EQUAL "FROM" Domain ({fromEmailDomain})\n')
992 | else:
993 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No Mismatch detected.{Fore.RESET}')
994 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.'))
995 | report.append(f'{self.indent}→ No Mismatch detected.\n')
996 |
997 | else:
998 | #print(f'{Fore.WHITE}No Message-ID Field detected. Skipping...{Fore.RESET}')
999 | print(self.colors.white("No Message-ID Field detected. Skipping..."))
1000 | report.append('No Message-ID Field detected. Skipping...\n')
1001 |
1002 |
1003 | if fromMatch.group(1) is not None and content['reply-to'] is not None:
1004 |
1005 | #print(f'{Fore.LIGHTGREEN_EX}Reply-To Field detected !{Fore.RESET}')
1006 | print(self.colors.green("Reply-To Field detected !"))
1007 | report.append('Reply-To Field detected !')
1008 |
1009 | if formatReplyTo == False:
1010 | if content['from'].strip().lower() != content['reply-to'].strip().lower():
1011 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]}){Fore.RESET}')
1012 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})'))
1013 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})')
1014 |
1015 | else:
1016 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}')
1017 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.'))
1018 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')
1019 |
1020 | elif formatReplyTo == True:
1021 | if fromMatch.group(1).strip().lower() != replyTo.group(1).strip().lower():
1022 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)}){Fore.RESET}')
1023 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})'))
1024 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})')
1025 |
1026 | else:
1027 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}')
1028 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.'))
1029 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')
1030 |
1031 | else:
1032 | #print(f'{Fore.WHITE}No Reply-To Field detected. Skipping...{Fore.RESET}')
1033 | print(self.colors.white("No Reply-To Field detected. Skipping..."))
1034 | report.append('No Reply-To Field detected. Skipping...\n')
1035 |
1036 | if fromMatch.group(1) is not None and content['return-path'] is not None:
1037 |
1038 | #print(f'{Fore.LIGHTGREEN_EX}Return-Path Field detected !{Fore.RESET}')
1039 | print(self.colors.green("Return-Path Field detected !"))
1040 | report.append('\nReturn-Path Field detected !')
1041 |
1042 | if formatReturnPath == False:
1043 | if fromMatch.group(1).strip().lower() != content['return-path'].strip().lower():
1044 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]}){Fore.RESET}')
1045 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})'))
1046 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})')
1047 |
1048 | else:
1049 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.'))
1050 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')
1051 |
1052 | elif formatReturnPath == True:
1053 | if fromMatch.group(1).strip().lower() != returnToPath.group(1).strip().lower():
1054 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})'))
1055 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})')
1056 |
1057 | else:
1058 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.'))
1059 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')
1060 |
1061 | else:
1062 | print(self.colors.white("No Return-Path Field detected. Skipping..."))
1063 | report.append('No Return-Path Field detected. Skipping...')
1064 |
1065 | #------------------------Check with VirusTotal------------------------#
1066 |
1067 | #print(f'\n{Fore.LIGHTYELLOW_EX}Note: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.{Fore.RESET}')
1068 | print(self.colors.yellow("\nNote: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code."))
1069 |
1070 | print(self.colors.magenta("Checking with VirusTotal..."))
1071 | report.append('\n\nChecking with VirusTotal...\n')
1072 |
1073 | if filteredIpv4:
1074 | # If you got an VT API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
1075 | #os.system(f'curl -s -X GET --header "x-apikey: " "https://www.virustotal.com/api/v3/ip_addresses/{ipv4[0]}" > vt.json')
1076 |
1077 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection")}')
1078 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations")}')
1079 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph")}')
1080 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic")}')
1081 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois")}')
1082 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments")}')
1083 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes")}')
1084 |
1085 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection\n')
1086 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations\n')
1087 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph\n')
1088 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic\n')
1089 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois\n')
1090 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments\n')
1091 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes\n')
1092 |
1093 |
1094 | elif authResultOrigIP:
1095 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection")}'),
1096 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations")}')
1097 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph")}')
1098 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic")}')
1099 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois")}')
1100 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments")}')
1101 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes")}')
1102 |
1103 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection\n')
1104 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations\n')
1105 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph\n')
1106 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic\n')
1107 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois\n')
1108 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments\n')
1109 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes\n')
1110 |
1111 |
1112 |
1113 | else:
1114 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}')
1115 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
1116 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
1117 |
1118 | #------------------------Check with AbuseIPDB------------------------#
1119 |
1120 | print(self.colors.magenta("\nChecking with AbuseIPDB..."))
1121 | report.append('\nChecking with AbuseIPDB...\n')
1122 |
1123 | if filteredIpv4:
1124 | # If you got an AbuseIPDB API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
1125 | #os.system(f'curl -s -X GET --header "Key: " "https://api.abuseipdb.com/api/v2/check?ipAddress={ipv4[0]}" > abuseipdb.json')
1126 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{filteredIpv4[0]}")}')
1127 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{filteredIpv4[0]}')
1128 |
1129 | elif authResultOrigIP:
1130 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{authResultOrigIP[0]}")}')
1131 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{authResultOrigIP[0]}')
1132 |
1133 | else:
1134 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
1135 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
1136 |
1137 | #------------------------Check with IPQualityScore------------------------#
1138 |
1139 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking with IPQualityScore...{Fore.RESET}')
1140 | print(self.colors.magenta("\nChecking with IPQualityScore..."))
1141 | report.append('\n\nChecking with IPQualityScore...\n')
1142 |
1143 | if filteredIpv4:
1144 | # If you got an IPQualityScore API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key.
1145 | #os.system(f'curl -s -X GET --header "Key: " "https://www.ipqualityscore.com/api/json/ip//{ipv4[0]}" > ipqualityscore.json')
1146 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}{Fore.RESET}')
1147 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}")}')
1148 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}')
1149 |
1150 | elif authResultOrigIP:
1151 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}{Fore.RESET}')
1152 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}")}')
1153 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}')
1154 |
1155 | else:
1156 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}')
1157 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required."))
1158 | report.append(f'Could not detect SMTP Server. Manual reviewing required.')
1159 |
1160 | return ''.join(report)
--------------------------------------------------------------------------------
/core/utils.py:
--------------------------------------------------------------------------------
1 | from core.colors import Colors
2 | from email.parser import BytesParser
3 | from email.header import decode_header
4 | import dns.resolver
5 | import re
6 | from datetime import datetime
7 | import sys
8 | import hashlib
9 |
10 |
11 | class Utils:
12 | def __init__(self, header):
13 | self.eHeader = header
14 | self.colors = Colors()
15 |
16 | def getFields(self):
17 | fields = []
18 | # First find all the fields present in the email headers
19 | with open(self.eHeader, "rb") as fp:
20 | headers = BytesParser().parse(fp)
21 |
22 | # Add each field to a list
23 | for j in headers:
24 | fields.append(j + ":")
25 |
26 | return fields
27 |
28 |
29 | def getReceivedFields(self):
30 | # credits goes to spcnvdr for helping me with this part of the code. https://github.com/spcnvdr/tracemail/tree/master Copyright 2020 spcnvdr
31 |
32 | #------------------------get all the "Receveid: from" Fields------------------------#
33 |
34 | found = False
35 | tmp = ''
36 | receivedFields =[]
37 | finalReceivedFields = []
38 | fields = self.getFields()
39 |
40 | with open(self.eHeader, 'r', encoding='UTF-8') as header:
41 | for lines in header:
42 | separator = lines.split()
43 |
44 | if len(separator) != 0 and separator[0] in fields and found:
45 | receivedFields.append(tmp)
46 | tmp =''
47 | if separator[0] != 'Received:':
48 | found = False
49 | else:
50 | tmp += lines
51 | elif found:
52 | tmp += lines
53 | elif 'Received:' in lines.split():
54 | found = True
55 | tmp += lines
56 |
57 | for x in receivedFields:
58 | finalReceivedFields.append(' '.join(x.split()))
59 |
60 | return finalReceivedFields
61 |
62 | def resolveIP(self, domain):
63 | try:
64 | resolve4 = dns.resolver.resolve(domain, 'A')
65 | #resolve6 = dns.resolver.resolve(domain, 'AAAA')
66 | if resolve4:
67 | for resolved4 in resolve4:
68 | return f'{resolved4}'
69 | except:
70 | return f'{self.colors.red("Error.")}'
71 |
72 |
73 | def routing(self):
74 |
75 | routing =[]
76 | counter= 0 # counter for the hops
77 |
78 | print(f'\n{self.colors.light_blue("Relay Routing: ")}')
79 | routing.append(f'Relay Routing:\n')
80 |
81 | for y in reversed(self.getReceivedFields()):
82 |
83 | # Regex for the field values
84 | receivedMatch = re.findall(r'received: from ([\w\-.:]+)', y, re.IGNORECASE)
85 | byMatch = re.findall(r'by ([\w\-.:]+)', y, re.IGNORECASE)
86 | withMatch = re.findall(r'with ([\w\-.:]+)', y, re.IGNORECASE)
87 |
88 | counter += 1
89 | try:
90 | if len(receivedMatch) != 0:
91 | print(f'Hop {counter} |↓|: FROM {self.colors.green(receivedMatch[0].lower())} TO {self.colors.green(byMatch[0].lower())} WITH {self.colors.cyan(withMatch[0].lower())}')
92 | routing.append(f'Hop {counter} |↓|: FROM {receivedMatch[0].lower()} TO {byMatch[0].lower()} WITH {withMatch[0].lower()}\n')
93 | else:
94 | print(f'{self.colors.yellow("No match found for Hop " + str(counter))}')
95 | except Exception as e:
96 | print(f'{self.colors.red("Error: " + str(e) + ". Skipping...")}')
97 |
98 | print(f'\n{self.colors.light_blue("Timestamps between Hops: ")}')
99 | routing.append(f'\nTimestamps between Hops:\n')
100 |
101 | dateCounter = 1 #separate counter for the hops in the timestamps
102 | prevTimestamp = None
103 | delta = None
104 |
105 | for x in reversed(self.getReceivedFields()):
106 | # There are potentialy 3 different date formats in the received fields.
107 | # That's why we have to check for each one of them.
108 | dateMatch1 = re.findall(r'\S{3},[ ]{0,4} \d{1,2} \S{3} \d{1,4} \d{2}:\d{2}:\d{2} [+-]\d{4}', x ,re.IGNORECASE)
109 | dateMatch2 = re.findall(r'\S{3}, \d{2} \S{3} \d{1,4} \d{2}:\d{2}:\d{2}.\d{0,10} [+-]\d{4}', x, re.IGNORECASE)
110 | dateMatch3 = re.findall(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{0,10} [+-]\d{4}', x, re.IGNORECASE)
111 |
112 | if dateMatch1 is not None:
113 | for date in reversed(dateMatch1):
114 | currentTimeStamp = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S %z')
115 | if prevTimestamp:
116 | delta = currentTimeStamp - prevTimestamp
117 | print(f'Hop {dateCounter}: {self.colors.green(date)}, {self.colors.cyan("Delta: " + str(delta))}')
118 | routing.append(f'Hop {dateCounter}: {date}, Delta: {delta if delta is not None else "N/A"}\n')
119 | dateCounter += 1
120 | prevTimestamp = currentTimeStamp
121 |
122 | elif dateMatch2 is not None:
123 | for date in reversed(dateMatch2):
124 | currentTimeStamp = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S.%f %z')
125 | if prevTimestamp:
126 | delta = currentTimeStamp - prevTimestamp
127 | print(f'Hop {dateCounter}: {self.colors.green(date)}, {self.colors.cyan("Delta: " + str(delta))}')
128 | routing.append(f'Hop {dateCounter}: {date}, Delta: {delta if delta is not None else "N/A"}\n')
129 | dateCounter += 1
130 | prevTimestamp = currentTimeStamp
131 |
132 | elif dateMatch3 is not None:
133 | for date in reversed(dateMatch3):
134 | currentTimeStamp = datetime.strptime(date, '%Y-%m-%d %H:%M:%S.%f %z')
135 | if prevTimestamp:
136 | delta = currentTimeStamp - prevTimestamp
137 | print(f'Hop {dateCounter}: {self.colors.green(date)}, {self.colors.cyan("Delta: " + str(delta))}')
138 | routing.append(f'Hop {dateCounter}: {date}\n, Delta: {delta if delta is not None else "N/A"}\n')
139 | dateCounter += 1
140 | prevTimestamp = currentTimeStamp
141 |
142 | return ''.join(routing)
143 |
144 |
145 | def generalInformation(self):
146 | try:
147 | with open(self.eHeader, 'r', encoding='UTF-8') as header:
148 | raw_content = header.read()
149 | content = BytesParser().parsebytes(raw_content.encode('UTF-8'))
150 | except FileNotFoundError:
151 | print(f'{self.colors.red("File not found.")}')
152 | sys.exit(1)
153 | except Exception as e:
154 | print(f'{self.colors.red(f"Error reading file: {e}")}')
155 | sys.exit(1)
156 |
157 | # Trying to decode the subject
158 | subject = content.get('subject', 'No subject found')
159 | if subject != 'No subject found':
160 | decoded_subject = decode_header(subject)
161 | decodedHeader = ''
162 | for part, charset in decoded_subject:
163 | try:
164 | decodedHeader += part.decode(charset or 'utf8') if isinstance(part, bytes) else part
165 | except UnicodeDecodeError:
166 | decodedHeader += part.decode('iso-8859-1') if isinstance(part, bytes) else part
167 | else:
168 | decodedHeader = subject
169 |
170 | print(f'\n{self.colors.light_blue("General Information:")}')
171 |
172 | from_field = content.get("from", "No 'From' field found")
173 | to_field = content.get("to", "No 'To' field found")
174 | date_field = content.get("date", "No 'Date' field found")
175 |
176 | print(f'From: {self.colors.green(from_field)}')
177 | print(f'To: {self.colors.green(to_field)}')
178 | print(f'Subject: {self.colors.green(decodedHeader)}')
179 | print(f'Date: {self.colors.green(date_field)}')
180 |
181 | return f'From: {from_field}\nTo: {to_field}\nSubject: {decodedHeader}\nDate: {date_field}'
182 |
183 |
184 | def securityInformations(self):
185 | try:
186 | with open(self.eHeader, 'r', encoding='UTF-8') as header:
187 | content = BytesParser().parsebytes(header.read().encode('UTF-8'))
188 | except FileNotFoundError:
189 | self.colors.red("File not found.")
190 | sys.exit(1)
191 |
192 | secInfos = []
193 |
194 | print(f'\n{self.colors.light_blue("Security Informations: ")}')
195 | secInfos.append(f'\nSecurity Informations:\n')
196 |
197 | if content['received-spf'] is not None:
198 | if 'fail' in content['received-spf'].lower():
199 | print(f'Received SPF: {self.colors.red(content["received-spf"])}')
200 | secInfos.append(f'Received SPF: {content["received-spf"]}')
201 |
202 | elif 'None' in content['received-spf']:
203 | print(f'Received SPF: {self.colors.red(content["received-spf"])}')
204 | secInfos.append(f'Received SPF: {content["received-spf"]}')
205 |
206 | else:
207 | print(f'Received SPF: {self.colors.green(content["received-spf"])}')
208 | secInfos.append(f'Received SPF: {content["received-spf"]}')
209 | else:
210 | print(f'Received SPF: {self.colors.red("No Received SPF")}')
211 | secInfos.append(f'Received SPF: No Received SPF')
212 |
213 |
214 | if content['dkim-signature'] is not None:
215 | print(f'DKIM Signature: {self.colors.green(content["dkim-signature"])}')
216 | secInfos.append(f'DKIM Signature: {content["dkim-signature"]}')
217 | else:
218 | print(f'DKIM Signature: {self.colors.red("No DKIM Signature")}')
219 | secInfos.append(f'DKIM Signature: No DKIM Signature')
220 |
221 |
222 | if content['dmarc'] is not None:
223 | print(f'DMARC: {self.colors.green(content["dmarc"])}')
224 | secInfos.append(f'DMARC: {content["dmarc"]}')
225 | else:
226 | print(f'DMARC: {self.colors.red("No DMARC")}')
227 | secInfos.append(f'DMARC: No DMARC')
228 |
229 |
230 | if content['authentication-results'] is not None:
231 |
232 | if 'spf=fail' in content['authentication-results'].lower():
233 | print(f'Authentication Results: {self.colors.red(content["authentication-results"])}')
234 | secInfos.append(f'Authentication Results: {content["authentication-results"]}')
235 | else:
236 | print(f'Authentication Results: {self.colors.green(content["authentication-results"])}')
237 | secInfos.append(f'Authentication Results: {content["authentication-results"]}')
238 |
239 | else:
240 | print(f'Authentication Results: {self.colors.red("No Authentication Results")}')
241 | secInfos.append(f'Authentication Results: No Authentication Results')
242 |
243 |
244 | if content['x-forefront-antispam-report'] is not None:
245 | print(f'X-Forefront-Antispam-Report: {self.colors.green(content["x-forefront-antispam-report"])}')
246 | secInfos.append(f'X-Forefront-Antispam-Report: {content["x-forefront-antispam-report"]}')
247 | else:
248 | print(f'X-Forefront-Antispam-Report: {self.colors.red("No X-Forefront-Antispam-Report")}')
249 | secInfos.append(f'X-Forefront-Antispam-Report: No X-Forefront-Antispam-Report')
250 |
251 |
252 | if content['x-microsoft-antispam'] is not None:
253 | print(f'X-Microsoft-Antispam: {self.colors.green(content["x-microsoft-antispam"])}')
254 | secInfos.append(f'X-Microsoft-Antispam: {content["x-microsoft-antispam"]}')
255 | else:
256 | print(f'X-Microsoft-Antispam: {self.colors.red("No X-Microsoft-Antispam")}')
257 | secInfos.append(f'X-Microsoft-Antispam: No X-Microsoft-Antispam')
258 |
259 | return '\n'.join(secInfos)
260 |
261 | def envelope(self):
262 | try:
263 | with open(self.eHeader, 'r', encoding='UTF-8') as header:
264 | content = BytesParser().parsebytes(header.read().encode('UTF-8'))
265 | except FileNotFoundError:
266 | self.colors.red("File not found.")
267 | sys.exit(1)
268 |
269 | eenvelope = []
270 |
271 | print(f'\n{self.colors.light_blue("Other Interesting Headers: ")}')
272 | eenvelope.append(f'\nOther Interesting Headers:\n')
273 |
274 |
275 | if content['X-ORIG-EnvelopeFrom'] is not None:
276 | fromMatch = re.search(r'<(.*)>', content['from'])
277 |
278 | if content['X-ORIG-EnvelopeFrom'] == 'anonymous@':
279 | print(f'Envelope From: {self.colors.red(content["X-ORIG-EnvelopeFrom"])}')
280 | eenvelope.append(f'Envelope From: {content["X-ORIG-EnvelopeFrom"]}')
281 |
282 | elif content['X-ORIG-EnvelopeFrom'] != fromMatch.group(1):
283 | from_address = content['from']
284 | orig_envelope_from = content['X-ORIG-EnvelopeFrom']
285 | message = f"POTENTIAL SPOOFING ATTACK DETECTED: FROM ({from_address}) NOT EQUAL ({orig_envelope_from})"
286 | print(f'Envelope From: {self.colors.red(message)}')
287 | eenvelope.append(message)
288 |
289 | else:
290 | print(f'Envelope From: {self.colors.green(content["X-ORIG-EnvelopeFrom"])}')
291 | eenvelope.append(f'Envelope From: {content["X-ORIG-EnvelopeFrom"]}')
292 | else:
293 | print(f'Envelope From: {self.colors.red("No Envelope From")}')
294 | eenvelope.append(f'Envelope From: No Envelope From')
295 |
296 |
297 | if content['return-path'] is not None:
298 | print(f'Return Path: {self.colors.green(content["return-path"])}')
299 | eenvelope.append(f'Return Path: {content["return-path"]}')
300 | else:
301 | print(f'Return Path: {self.colors.red("No Return Path")}')
302 | eenvelope.append(f'Return Path: No Return Path')
303 |
304 | if content['message-id'] is not None:
305 | print(f'Message ID: {self.colors.green(content["message-id"])}')
306 | eenvelope.append(f'Message ID: {content["message-id"]}')
307 | else:
308 | print(f'Message ID: {self.colors.red("No Message ID")}')
309 | eenvelope.append(f'Message ID: No Message ID')
310 |
311 | if content['mime-version'] is not None:
312 | print(f'MIME-Version: {self.colors.green(content["mime-version"])}')
313 | eenvelope.append(f'MIME-Version: {content["mime-version"]}')
314 | else:
315 | print(f'MIME-Version: {self.colors.red("No MIME-Version")}')
316 | eenvelope.append(f'MIME-Version: No MIME-Version')
317 |
318 | if content['authentication-results-original'] is not None:
319 | if 'spf=fail' in content['authentication-results-original'].lower():
320 | print(f'Authentication-Results-Original: {self.colors.red(content["authentication-results-original"])}')
321 | eenvelope.append(f'Authentication-Results-Original: {content["authentication-results-original"]}')
322 |
323 | elif 'spf=pass' in content['authentication-results-original'].lower():
324 | print(f'Authentication-Results-Original: {self.colors.green(content["authentication-results-original"])}')
325 | eenvelope.append(f'Authentication-Results-Original: {content["authentication-results-original"]}')
326 |
327 | else:
328 | print(f'Authentication-Results-Original: {self.colors.yellow(content["authentication-results-original"])}')
329 | eenvelope.append(f'Authentication-Results-Original: {content["authentication-results-original"]}')
330 | else:
331 | print(f'Authentication-Results-Original: {self.colors.red("No Authentication-Results-Original")}')
332 | eenvelope.append(f'Authentication-Results-Original: No Authentication-Results-Original')
333 |
334 | message = '\n<---------MS Exchange Organization Headers--------->\n'
335 | print(f'{self.colors.cyan(message)}')
336 | eenvelope.append(message)
337 |
338 | if content['x-ms-exchange-organization-authas'] is not None:
339 | if 'anonymous' or 'Anonymous' in content['x-ms-exchange-organization-authas']:
340 | print(f'X-MS-Exchange-Organization-AuthAs: {self.colors.yellow(content["x-ms-exchange-organization-authas"])}')
341 | eenvelope.append(f'X-MS-Exchange-Organization-AuthAs: {content["x-ms-exchange-organization-authas"]}')
342 | else:
343 | print(f'X-MS-Exchange-Organization-AuthAs: {self.colors.green(content["x-ms-exchange-organization-authas"])}')
344 | eenvelope.append(f'X-MS-Exchange-Organization-AuthAs: {content["x-ms-exchange-organization-authas"]}')
345 | else:
346 | print(f'X-MS-Exchange-Organization-AuthAs: {self.colors.red("No X-MS-Exchange-Organization-AuthAs")}')
347 | eenvelope.append(f'X-MS-Exchange-Organization-AuthAs: No X-MS-Exchange-Organization-AuthAs')
348 |
349 | if content['x-ms-exchange-organization-authsource'] is not None:
350 | print(f'X-MS-Exchange-Organization-AuthSource: {self.colors.green(content["x-ms-exchange-organization-authsource"])}')
351 | eenvelope.append(f'X-MS-Exchange-Organization-AuthSource: {content["x-ms-exchange-organization-authsource"]}')
352 | else:
353 | print(f'X-MS-Exchange-Organization-AuthSource: {self.colors.red("No X-MS-Exchange-Organization-AuthSource")}')
354 | eenvelope.append(f'X-MS-Exchange-Organization-AuthSource: No X-MS-Exchange-Organization-AuthSource')
355 |
356 | if content['x-ms-exchange-organization-authmechanism'] is not None:
357 | print(f'X-MS-Exchange-Organization-AuthMechanism: {self.colors.green(content["x-ms-exchange-organization-authmechanism"])}')
358 | eenvelope.append(f'X-MS-Exchange-Organization-AuthMechanism: {content["x-ms-exchange-organization-authmechanism"]}')
359 | else:
360 | print(f'X-MS-Exchange-Organization-AuthMechanism: {self.colors.red("No X-MS-Exchange-Organization-AuthMechanism")}')
361 | eenvelope.append(f'X-MS-Exchange-Organization-AuthMechanism: No X-MS-Exchange-Organization-AuthMechanism')
362 |
363 | if content['x-ms-exchange-organization-network-message-id'] is not None:
364 | print(f'X-MS-Exchange-Organization-Network-Message-Id: {self.colors.green(content["x-ms-exchange-organization-network-message-id"])}')
365 | eenvelope.append(f'X-MS-Exchange-Organization-Network-Message-Id: {content["x-ms-exchange-organization-network-message-id"]}')
366 | else:
367 | #print(f'X-MS-Exchange-Organization-Network-Message-Id: {Fore.LIGHTRED_EX}No X-MS-Exchange-Organization-Network-Message-Id{Fore.RESET}')
368 | print(f'X-MS-Exchange-Organization-Network-Message-Id: {self.colors.red("No X-MS-Exchange-Organization-Network-Message-Id")}')
369 | eenvelope.append(f'X-MS-Exchange-Organization-Network-Message-Id: No X-MS-Exchange-Organization-Network-Message-Id')
370 |
371 | message = '\n<-------------------------------------------------->\n'
372 | print(f'{self.colors.cyan(message)}')
373 | eenvelope.append(message)
374 |
375 | return '\n'.join(eenvelope)
376 |
377 | def check_attachment(self, attachment):
378 | result = []
379 | indent = " "
380 |
381 | #print(f'\n\n{Fore.LIGHTBLUE_EX}Checking the attachment...{Fore.RESET}')
382 | print(Colors.light_blue("\nChecking the attachment..."))
383 | result.append('\n\nChecking the attachment...\n')
384 |
385 | sha256 = hashlib.sha256()
386 | BUFFER = 65536
387 |
388 | with open(attachment, 'rb') as file:
389 | while True:
390 | data = file.read(BUFFER)
391 | if not data:
392 | break
393 |
394 | sha256.update(data)
395 |
396 | #print(f'{indent}--> Link: {Fore.LIGHTGREEN_EX}https://www.virustotal.com/gui/file/{sha256.hexdigest()}/detection{Fore.RESET}')
397 | print(f'{indent}--> Link: {Colors.green(f"https://www.virustotal.com/gui/file/{sha256.hexdigest()}/detection")}')
398 | result.append(f'--> Link: https://www.virustotal.com/gui/file/{sha256.hexdigest()}/detection')
399 |
400 | return '\n'.join(result)
401 |
402 |
403 |
--------------------------------------------------------------------------------
/dist/elyzer.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mochabyte0x/Elyzer/132da05e1830aac57ac7f5f1266c3d725a6c05c2/dist/elyzer.exe
--------------------------------------------------------------------------------
/elyzer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import re
4 | import json
5 | import requests
6 |
7 | from core.utils import Utils
8 | from core.spoofing import Spoofing
9 | from argparse import ArgumentParser
10 | from core.colors import Colors, banner
11 | from core.exp_json import ExportJson, export_to_json
12 |
13 |
14 | def checkForUpdates():
15 | try:
16 | response = requests.get('https://api.github.com/repos/B0lg0r0v/Elyzer/releases/latest')
17 | except requests.exceptions.ConnectionError:
18 | #print(f'{Fore.RED}No internet connection.{Fore.RESET}')
19 | print(Colors.red("No internet connection."))
20 | exit()
21 |
22 | latestRelease = json.loads(response.text)
23 |
24 | if 'tag_name' in latestRelease:
25 | latestVersion = latestRelease['tag_name'].lower()
26 |
27 | match = re.search(r'v\d+\.\d\.\d+', latestVersion) #Extract only the version number
28 | if match:
29 | latestVersion = match.group(0)
30 |
31 | if CURRENT_VERSION != latestVersion:
32 | if latestVersion > CURRENT_VERSION:
33 | print(f'A new version ({latestVersion}) is available. Please download it from the release section on GitHub.\n')
34 | elif latestVersion == CURRENT_VERSION:
35 | pass
36 | elif latestVersion < CURRENT_VERSION:
37 | pass
38 |
39 |
40 | if __name__ == '__main__':
41 |
42 | indent = ' ' * 3
43 | CURRENT_VERSION = 'v0.5.0'
44 | savings = []
45 |
46 | checkForUpdates()
47 |
48 | parser = ArgumentParser() # Create the Parser.
49 | parser.add_argument('-f', '--file', help='Give the E-Mail Header as a file.', required=True)
50 | parser.add_argument('-pa', '--passive', help='Enables the passive mode. DNS resolution is performed passively through Driftnet for better OPSEC. You need to add "DRIFTNET_API" as an environment variable in order to use this feature.', action='store_true')
51 | parser.add_argument('-nd', '--no-dns', help='Enables the no-dns mode. No DNS resolution is performed for best OPSEC. This heavily affects the results !', action='store_true')
52 | parser.add_argument('-q', '--quiet', help='Quiet mode. Disables banner.', action='store_true')
53 | parser.add_argument('-j', '--json', help='EXPERIMENTAL FEATURE. Output the results in JSON format.', action='store_true')
54 | parser.add_argument('-v', '--version', action='version', version=f'Elyzer {CURRENT_VERSION}')
55 | parser.add_argument('-a', '--attachement', help='Check if the file is malicious.')
56 | args = parser.parse_args() # Initialize the Parser.
57 |
58 |
59 | if args.file is not None:
60 | utils = Utils(args.file) # Initialize the Utils class.
61 | spoofing = Spoofing(args.file) # Initialize the Spoofing class.
62 | expjson = ExportJson(args.file)
63 | current_dir = os.path.dirname(os.path.abspath(__file__))
64 |
65 | if args.quiet:
66 |
67 | print(Colors.yellow("E-Mail Header Analysis complete"))
68 | if args.passive:
69 |
70 | if args.json:
71 | if args.attachement:
72 | utils.generalInformation()
73 | utils.routing()
74 | utils.securityInformations()
75 | utils.envelope()
76 | spoofing.spoofing_passive_dns()
77 | utils.check_attachment(args.attachement)
78 |
79 | json_output = expjson.jsonoutput()
80 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json"))
81 | print(f'\nElyzer.json has been saved to {json_file}')
82 |
83 | else:
84 | utils.generalInformation()
85 | utils.routing()
86 | utils.securityInformations()
87 | utils.envelope()
88 | spoofing.spoofing_passive_dns()
89 |
90 | json_output = expjson.jsonoutput()
91 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json"))
92 | print(f'\nElyzer.json has been saved to {json_file}')
93 |
94 | else:
95 | if args.attachement:
96 | utils.generalInformation()
97 | utils.routing()
98 | utils.securityInformations()
99 | utils.envelope()
100 | spoofing.spoofing_passive_dns()
101 | utils.check_attachment(args.attachement)
102 |
103 | else:
104 | utils.generalInformation()
105 | utils.routing()
106 | utils.securityInformations()
107 | utils.envelope()
108 | spoofing.spoofing_passive_dns()
109 |
110 | elif args.no_dns:
111 |
112 | if args.json:
113 |
114 | if args.attachement:
115 | utils.generalInformation()
116 | utils.routing()
117 | utils.securityInformations()
118 | utils.envelope()
119 | spoofing.spoofing_no_dns()
120 | utils.check_attachment(args.attachement)
121 |
122 | json_output = expjson.jsonoutput()
123 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json"))
124 | print(f'\nElyzer.json has been saved to {json_file}')
125 |
126 | else:
127 | utils.generalInformation()
128 | utils.routing()
129 | utils.securityInformations()
130 | utils.envelope()
131 | spoofing.spoofing_no_dns()
132 |
133 | json_output = expjson.jsonoutput()
134 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json"))
135 | print(f'\nElyzer.json has been saved to {json_file}')
136 |
137 | else:
138 | if args.attachement:
139 | utils.generalInformation()
140 | utils.routing()
141 | utils.securityInformations()
142 | utils.envelope()
143 | spoofing.spoofing_no_dns()
144 | utils.check_attachment(args.attachement)
145 |
146 | else:
147 | utils.generalInformation()
148 | utils.routing()
149 | utils.securityInformations()
150 | utils.envelope()
151 | spoofing.spoofing_no_dns()
152 |
153 | else:
154 |
155 | if args.json:
156 | if args.attachement:
157 | utils.generalInformation()
158 | utils.routing()
159 | utils.securityInformations()
160 | utils.envelope()
161 | spoofing.spoofing_all_checks()
162 | utils.check_attachment(args.attachement)
163 |
164 | json_output = expjson.jsonoutput()
165 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json"))
166 | print(f'\nElyzer.json has been saved to {json_file}')
167 |
168 | else:
169 | utils.generalInformation()
170 | utils.routing()
171 | utils.securityInformations()
172 | utils.envelope()
173 | spoofing.spoofing_all_checks()
174 |
175 | json_output = expjson.jsonoutput()
176 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json"))
177 | print(f'\nElyzer.json has been saved to {json_file}')
178 |
179 | else:
180 | if args.attachement:
181 | utils.generalInformation()
182 | utils.routing()
183 | utils.securityInformations()
184 | utils.envelope()
185 | spoofing.spoofing_all_checks()
186 | utils.check_attachment(args.attachement)
187 |
188 | else:
189 | utils.generalInformation()
190 | utils.routing()
191 | utils.securityInformations()
192 | utils.envelope()
193 | spoofing.spoofing_all_checks()
194 |
195 | else:
196 | banner()
197 | print(Colors.yellow("E-Mail Header Analysis complete"))
198 | if args.passive:
199 |
200 | if args.attachement:
201 | utils.generalInformation()
202 | utils.routing()
203 | utils.securityInformations()
204 | utils.envelope()
205 | spoofing.spoofing_passive_dns()
206 | utils.check_attachment(args.attachement)
207 |
208 | else:
209 | utils.generalInformation()
210 | utils.routing()
211 | utils.securityInformations()
212 | utils.envelope()
213 | spoofing.spoofing_passive_dns()
214 |
215 | elif args.no_dns:
216 |
217 | if args.attachement:
218 | utils.generalInformation()
219 | utils.routing()
220 | utils.securityInformations()
221 | utils.envelope()
222 | spoofing.spoofing_no_dns()
223 | utils.check_attachment(args.attachement)
224 |
225 | else:
226 | utils.generalInformation()
227 | utils.routing()
228 | utils.securityInformations()
229 | utils.envelope()
230 | spoofing.spoofing_no_dns()
231 |
232 | else:
233 |
234 | if args.json:
235 |
236 | if args.attachement:
237 | utils.generalInformation()
238 | utils.routing()
239 | utils.securityInformations()
240 | utils.envelope()
241 | spoofing.spoofing_all_checks()
242 | utils.check_attachment(args.attachement)
243 |
244 | json_output = expjson.jsonoutput()
245 | json_file = export_to_json(json_output)
246 | print(f'\nElyzer.json has been saved to {json_file}')
247 |
248 | else:
249 | utils.generalInformation()
250 | utils.routing()
251 | utils.securityInformations()
252 | utils.envelope()
253 | spoofing.spoofing_all_checks()
254 |
255 | json_output = expjson.jsonoutput()
256 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json"))
257 | print(f'\nElyzer.json has been saved to {json_file}')
258 |
259 | else:
260 | if args.attachement:
261 | utils.generalInformation()
262 | utils.routing()
263 | utils.securityInformations()
264 | utils.envelope()
265 | spoofing.spoofing_all_checks()
266 | utils.check_attachment(args.attachement)
267 |
268 | else:
269 | utils.generalInformation()
270 | utils.routing()
271 | utils.securityInformations()
272 | utils.envelope()
273 | spoofing.spoofing_all_checks()
274 |
275 | else:
276 | parser.error('E-Mail Header is required.')
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | colorama
2 | dnspython>=2.6.1
3 | Requests>=2.32.2
4 |
--------------------------------------------------------------------------------