├── requirements.txt
├── .github
├── banner.png
├── example.png
└── FUNDING.yml
├── README.md
└── FindUnusualSessions.py
/requirements.txt:
--------------------------------------------------------------------------------
1 | impacket
2 | sectools
3 | argparse
4 |
--------------------------------------------------------------------------------
/.github/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/p0dalirius/FindUnusualSessions/HEAD/.github/banner.png
--------------------------------------------------------------------------------
/.github/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/p0dalirius/FindUnusualSessions/HEAD/.github/example.png
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: p0dalirius
4 | patreon: Podalirius
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 | A tool to remotely detect unusual sessions opened on windows machines using RPC
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## Features
13 |
14 | - [x] Extracts the list of legitimate domains and trusts registered in the LDAP
15 | - [x] Maps the current sessions open on remote machines through RPC
16 | - [x] Highlight suspicious sessions
17 | - [x] Multithreaded connections to enum remote sessions.
18 | - [ ] Export results in JSON with `--export-json `.
19 | - [ ] Export results in XLSX with `--export-xlsx `.
20 | - [ ] Export results in SQLITE3 with `--export-sqlite `.
21 |
22 | ## Demonstration
23 |
24 | ```
25 | ./FindUnusualSessions.py -au $USER -ad $DOMAIN -ap $PASSWORD -ai $DC_IP
26 | ```
27 |
28 | 
29 |
30 | ## Usage
31 |
32 | ```
33 | $ ./FindUnusualSessions.py -h
34 | FindUnusualSessions v1.1 - by Remi GASCOU (Podalirius)
35 |
36 | usage: FindUnusualSessions.py [-h] [-v] [--debug] [--no-colors] [-L LOGFILE] [-t THREADS] [-ns NAMESERVER] [-tf TARGETS_FILE] [-tt TARGET] [-ad AUTH_DOMAIN] [-ai AUTH_DC_IP] [-au AUTH_USER] [--ldaps]
37 | [--no-ldap] [--subnets] [-tl TARGET_LDAP_QUERY] [--no-pass | -ap AUTH_PASSWORD | -ah AUTH_HASHES | --aes-key hex key] [-k] [--kdcHost AUTH_KDCHOST] [--export-xlsx EXPORT_XLSX]
38 | [--export-json EXPORT_JSON] [--export-sqlite EXPORT_SQLITE]
39 |
40 | options:
41 | -h, --help show this help message and exit
42 | -v, --verbose Verbose mode. (default: False).
43 | --debug Debug mode. (default: False).
44 | --no-colors Disables colored output mode.
45 | -L LOGFILE, --logfile LOGFILE
46 | File to write logs to.
47 | -t THREADS, --threads THREADS
48 | Number of threads (default: 64).
49 | -ns NAMESERVER, --nameserver NAMESERVER
50 | IP of the DNS server to use, instead of the --dc-ip.
51 |
52 | Targets:
53 | -tf TARGETS_FILE, --targets-file TARGETS_FILE
54 | Path to file containing a line by line list of targets.
55 | -tt TARGET, --target TARGET
56 | Target IP, FQDN or CIDR.
57 | -ad AUTH_DOMAIN, --auth-domain AUTH_DOMAIN
58 | Windows domain to authenticate to.
59 | -ai AUTH_DC_IP, --auth-dc-ip AUTH_DC_IP
60 | IP of the domain controller.
61 | -au AUTH_USER, --auth-user AUTH_USER
62 | Username of the domain account.
63 | --ldaps Use LDAPS (default: False)
64 | --no-ldap Do not perform LDAP queries.
65 | --subnets Get all subnets from the domain and use them as targets (default: False)
66 | -tl TARGET_LDAP_QUERY, --target-ldap-query TARGET_LDAP_QUERY
67 | LDAP query to use to extract computers from the domain.
68 |
69 | Credentials:
70 | --no-pass Don't ask for password (useful for -k)
71 | -ap AUTH_PASSWORD, --auth-password AUTH_PASSWORD
72 | Password of the domain account.
73 | -ah AUTH_HASHES, --auth-hashes AUTH_HASHES
74 | LM:NT hashes to pass the hash for this user.
75 | --aes-key hex key AES key to use for Kerberos Authentication (128 or 256 bits)
76 | -k, --kerberos Use Kerberos authentication. Grabs credentials from .ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the
77 | command line
78 | --kdcHost AUTH_KDCHOST
79 | IP of the domain controller.
80 |
81 | Output files:
82 | --export-xlsx EXPORT_XLSX
83 | Output XLSX file to store the results in.
84 | --export-json EXPORT_JSON
85 | Output JSON file to store the results in.
86 | --export-sqlite EXPORT_SQLITE
87 | Output SQLITE3 file to store the results in.
88 |
89 | ```
90 |
91 | ## Contributing
92 |
93 | Pull requests are welcome. Feel free to open an issue if you want to add other features.
94 |
--------------------------------------------------------------------------------
/FindUnusualSessions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # File name : FindUnusualSessions.py
4 | # Author : Podalirius (@podalirius_)
5 | # Date created : 11 April 2025
6 |
7 |
8 | from concurrent.futures import ThreadPoolExecutor
9 | from enum import Enum
10 | from impacket.dcerpc.v5 import transport, srvs
11 | from impacket.dcerpc.v5.ndr import NULL
12 | from impacket.dcerpc.v5.rpcrt import DCERPCException
13 | from impacket.smb3 import SMB3
14 | from impacket.smbconnection import SessionError
15 | from impacket.smb import SMB
16 | from sectools.network.domains import is_fqdn
17 | from sectools.network.ip import is_ipv4_cidr, is_ipv4_addr, is_ipv6_addr, expand_cidr
18 | from sectools.windows.crypto import parse_lm_nt_hashes
19 | from sectools.windows.ldap import get_computers_from_domain, get_subnets, raw_ldap_query, init_ldap_session
20 | import argparse
21 | import datetime
22 | import dns.resolver
23 | import os
24 | import re
25 | import socket
26 | import sys
27 | import threading
28 | import traceback
29 |
30 |
31 | VERSION = "1.1"
32 |
33 |
34 | def getDomainsAndTrusts(auth_domain, auth_username, auth_password, auth_lm_hash, auth_nt_hash, auth_dc_ip, use_ldaps=False):
35 | ldap_server, ldap_session = init_ldap_session(
36 | auth_domain=auth_domain,
37 | auth_dc_ip=auth_dc_ip,
38 | auth_username=auth_username,
39 | auth_password=auth_password,
40 | auth_lm_hash=auth_lm_hash,
41 | auth_nt_hash=auth_nt_hash,
42 | use_ldaps=use_ldaps
43 | )
44 |
45 | domainsAndtrusts = []
46 |
47 | ldapresults = list(ldap_session.extend.standard.paged_search(
48 | ldap_server.info.other["rootDomainNamingContext"][0],
49 | "(objectClass=domain)",
50 | attributes=["distinguishedName"]
51 | ))
52 |
53 | # Parse the DN to get the FQDN for all the entries
54 | for entry in ldapresults:
55 | if entry["type"] != "searchResEntry":
56 | continue
57 | dn = entry["attributes"]["distinguishedName"].upper()
58 | domain_parts = dn.split(',')
59 | domain = ""
60 | for part in domain_parts:
61 | if part.startswith("DC="):
62 | domain += part[3:] + "."
63 | domain = domain.rstrip('.')
64 | domainsAndtrusts.append(domain.upper())
65 |
66 | return domainsAndtrusts
67 |
68 |
69 | def timedeltaStr(t):
70 | hours, rem = divmod(t.total_seconds(), 3600)
71 | minutes, seconds = divmod(rem, 60)
72 | if hours > 0:
73 | return ("%dh%dm%ds" % (hours, minutes, seconds))
74 | elif minutes > 0:
75 | return ("%dm%ds" % (minutes, seconds))
76 | elif seconds > 0:
77 | return ("%ds" % (seconds))
78 |
79 |
80 | def export_xlsx(options, results):
81 | pass
82 |
83 |
84 | def export_sqlite(options, results):
85 | pass
86 |
87 |
88 | def export_json(options, results):
89 | pass
90 |
91 |
92 | def is_port_open(target, port) -> bool:
93 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
94 | s.settimeout(5)
95 | # Non-existant domains cause a lot of errors, added error handling
96 | try:
97 | return s.connect_ex((target, port)) == 0
98 | except Exception as e:
99 | return False
100 |
101 |
102 | def load_targets(options, logger):
103 | """
104 | Loads targets from various sources based on the provided options.
105 |
106 | This function populates the 'targets' list with domain computers, specific LDAP query results, and subnetworks of the domain.
107 | It checks the options provided and accordingly loads targets from different sources. If the 'no_ldap' option is set, it skips LDAP-based target loading.
108 | For domain computers, it uses either the default query or a specific LDAP query if provided. For subnetworks, it uses the provided subnets.
109 | """
110 |
111 | targets = []
112 |
113 | # Loading targets from domain computers
114 | if not options.no_ldap:
115 | if options.auth_dc_ip is not None and options.auth_user is not None and (options.auth_password is not None or options.auth_hashes is not None) and options.target_ldap_query is None:
116 | logger.debug("Loading targets from computers in the domain '%s'" % options.auth_domain)
117 | targets += get_computers_from_domain(
118 | auth_domain=options.auth_domain,
119 | auth_dc_ip=options.auth_dc_ip,
120 | auth_username=options.auth_user,
121 | auth_password=options.auth_password,
122 | auth_hashes=options.auth_hashes,
123 | auth_key=None,
124 | use_ldaps=options.ldaps,
125 | __print=False
126 | )
127 |
128 | # Loading targets from domain computers
129 | if not options.no_ldap:
130 | if options.auth_dc_ip is not None and options.auth_user is not None and (options.auth_password is not None or options.auth_hashes is not None) and options.target_ldap_query is not None:
131 | logger.debug("Loading targets from specfic LDAP query '%s'" % options.target_ldap_query)
132 | computers = raw_ldap_query(
133 | auth_domain=options.auth_domain,
134 | auth_dc_ip=options.auth_dc_ip,
135 | auth_username=options.auth_username,
136 | auth_password=options.auth_password,
137 | auth_hashes=options.auth_hashes,
138 | query=options.target_ldap_query,
139 | use_ldaps=options.use_ldaps,
140 | attributes=["dNSHostName"]
141 | )
142 | for _, computer in computers:
143 | targets.append(computer["dNSHostName"])
144 |
145 | # Loading targets from subnetworks of the domain
146 | if not options.no_ldap:
147 | if options.subnets and options.auth_dc_ip is not None and options.auth_user is not None and (options.auth_password is not None or options.auth_hashes is not None):
148 | logger.debug("Loading targets from subnetworks of the domain '%s'" % options.auth_domain)
149 | targets += get_subnets(
150 | auth_domain=options.auth_domain,
151 | auth_dc_ip=options.auth_dc_ip,
152 | auth_username=options.auth_user,
153 | auth_password=options.auth_password,
154 | auth_hashes=options.auth_hashes,
155 | auth_key=None,
156 | use_ldaps=options.ldaps,
157 | __print=True
158 | )
159 |
160 | # Loading targets line by line from a targets file
161 | if options.targets_file is not None:
162 | if os.path.exists(options.targets_file):
163 | logger.debug("Loading targets line by line from targets file '%s'" % options.targets_file)
164 | f = open(options.targets_file, "r")
165 | for line in f.readlines():
166 | targets.append(line.strip())
167 | f.close()
168 | else:
169 | logger.error("Could not open targets file '%s'" % options.targets_file)
170 |
171 | # Loading targets from a single --target option
172 | if len(options.target) != 0:
173 | logger.debug("Loading targets from --target options")
174 | for target in options.target:
175 | targets.append(target)
176 |
177 | # Sort uniq on targets list
178 | targets = sorted(list(set(targets)))
179 |
180 | final_targets = []
181 | # Parsing target to filter IP/DNS/CIDR
182 | for target in targets:
183 | if is_ipv4_cidr(target):
184 | final_targets += [("ip", ip) for ip in expand_cidr(target)]
185 | elif is_ipv4_addr(target):
186 | final_targets.append(("ipv4", target))
187 | elif is_ipv6_addr(target):
188 | final_targets.append(("ipv6", target))
189 | elif is_fqdn(target):
190 | final_targets.append(("fqdn", target))
191 | else:
192 | logger.debug("Target '%s' was not added." % target)
193 |
194 | final_targets = sorted(list(set(final_targets)))
195 |
196 | return final_targets
197 |
198 |
199 | class LogLevel(Enum):
200 | INFO = 0
201 | VERBOSE = 1
202 | DEBUG = 2
203 |
204 |
205 | class Logger(object):
206 | """
207 | A Logger class that provides logging functionalities with various levels such as INFO, DEBUG, WARNING, ERROR, and CRITICAL.
208 | It supports color-coded output, which can be disabled, and can also log messages to a file.
209 |
210 | Attributes:
211 | __debug (bool): If True, debug level messages will be printed and logged.
212 | __nocolors (bool): If True, disables color-coded output.
213 | logfile (str|None): Path to a file where logs will be written. If None, logging to a file is disabled.
214 |
215 | Methods:
216 | __init__(debug=False, logfile=None, nocolors=False): Initializes the Logger instance.
217 | print(message=""): Prints a message to stdout and logs it to a file if logging is enabled.
218 | info(message): Logs a message at the INFO level.
219 | debug(message): Logs a message at the DEBUG level if debugging is enabled.
220 | error(message): Logs a message at the ERROR level.
221 | """
222 |
223 | def __init__(self, loglevel, logfile=None, no_colors=False):
224 | super(Logger, self).__init__()
225 | self.no_colors = no_colors
226 | self.loglevel = loglevel
227 | self.logfile = logfile
228 | #
229 | if self.logfile is not None:
230 | if os.path.exists(self.logfile):
231 | k = 1
232 | while os.path.exists(self.logfile + (".%d"%k)):
233 | k += 1
234 | self.logfile = self.logfile + (".%d" % k)
235 | open(self.logfile, "w").close()
236 | self.debug("Writting logs to logfile: '%s'" % self.logfile)
237 |
238 | def print(self, message="", end='\n'):
239 | """
240 | Prints a message to stdout and logs it to a file if logging is enabled.
241 |
242 | This method prints the provided message to the standard output and also logs it to a file if a log file path is specified during the Logger instance initialization. The message can include color codes for color-coded output, which can be disabled by setting the `nocolors` attribute to True.
243 |
244 | Args:
245 | message (str): The message to be printed and logged.
246 | """
247 |
248 | nocolor_message = re.sub(r"\x1b[\[]([0-9;]+)m", "", message)
249 | if self.no_colors:
250 | print(nocolor_message, end=end)
251 | else:
252 | print(message, end=end)
253 | self.write_to_logfile(nocolor_message, end=end)
254 |
255 | def dateprint(self, message="", end='\n'):
256 | """
257 | Prints a message to stdout and logs it to a file if logging is enabled.
258 |
259 | This method prints the provided message to the standard output and also logs it to a file if a log file path is specified during the Logger instance initialization. The message can include color codes for color-coded output, which can be disabled by setting the `nocolors` attribute to True.
260 |
261 | Args:
262 | message (str): The message to be printed and logged.
263 | """
264 |
265 | date_str = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
266 | nocolor_message = re.sub(r"\x1b[\[]([0-9;]+)m", "", message)
267 | if self.no_colors:
268 | print("%s %s" % (date_str, nocolor_message), end=end)
269 | else:
270 | print("%s %s" % (date_str, message), end=end)
271 | self.write_to_logfile("%s %s" % (date_str, nocolor_message), end=end)
272 |
273 | def info(self, message):
274 | """
275 | Logs a message at the INFO level.
276 |
277 | This method logs the provided message at the INFO level. The message can include color codes for color-coded output, which can be disabled by setting the `nocolors` attribute to True. The message is also logged to a file if a log file path is specified during the Logger instance initialization.
278 |
279 | Args:
280 | message (str): The message to be logged at the INFO level.
281 | """
282 |
283 | date_str = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
284 | nocolor_message = re.sub(r"\x1b[\[]([0-9;]+)m", "", message)
285 | if self.no_colors:
286 | print("%s [info] %s" % (date_str, nocolor_message))
287 | else:
288 | print("%s [\x1b[1;92minfo\x1b[0m] %s" % (date_str, message))
289 | self.write_to_logfile("%s [info] %s" % (date_str, nocolor_message))
290 |
291 | def verbose(self, message):
292 | """
293 | Logs a message at the INFO level.
294 |
295 | This method logs the provided message at the INFO level. The message can include color codes for color-coded output, which can be disabled by setting the `nocolors` attribute to True. The message is also logged to a file if a log file path is specified during the Logger instance initialization.
296 |
297 | Args:
298 | message (str): The message to be logged at the INFO level.
299 | """
300 |
301 | if self.loglevel.value >= LogLevel.VERBOSE.value:
302 | date_str = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
303 | nocolor_message = re.sub(r"\x1b[\[]([0-9;]+)m", "", message)
304 | if self.no_colors:
305 | print("%s [verbose] %s" % (date_str, nocolor_message))
306 | else:
307 | print("%s [\x1b[1;92mverbose\x1b[0m] %s" % (date_str, message))
308 | self.write_to_logfile("%s [verbose] %s" % (date_str, nocolor_message))
309 |
310 | def debug(self, message):
311 | """
312 | Logs a message at the DEBUG level if debugging is enabled.
313 |
314 | This method logs the provided message at the DEBUG level if the `debug` attribute is set to True during the Logger instance initialization. The message can include color codes for color-coded output, which can be disabled by setting the `nocolors` attribute to True.
315 |
316 | Args:
317 | message (str): The message to be logged.
318 | """
319 |
320 | if self.loglevel.value >= LogLevel.DEBUG.value:
321 | date_str = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
322 | nocolor_message = re.sub(r"\x1b[\[]([0-9;]+)m", "", message)
323 | if self.no_colors:
324 | print("%s [debug] %s" % (date_str, nocolor_message))
325 | else:
326 | print("%s [debug] %s" % (date_str, message))
327 | self.write_to_logfile("%s [debug] %s" % (date_str, nocolor_message))
328 |
329 | def warn(self, message):
330 | """
331 | Logs an error message to the console and the log file.
332 |
333 | This method logs the provided error message to the standard error output and also logs it to a file if a log file path is specified during the Logger instance initialization. The message can include color codes for color-coded output, which can be disabled by setting the `nocolors` attribute to True.
334 |
335 | Args:
336 | message (str): The error message to be logged.
337 | """
338 |
339 | date_str = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
340 | nocolor_message = re.sub(r"\x1b[\[]([0-9;]+)m", "", message)
341 | if self.no_colors:
342 | print("%s [error] %s" % (date_str, nocolor_message))
343 | else:
344 | print("%s [\x1b[1;91merror\x1b[0m] %s" % (date_str, message))
345 | self.write_to_logfile("%s [error] %s" % (date_str, nocolor_message))
346 |
347 | def write_to_logfile(self, message, end='\n'):
348 | """
349 | Writes the provided message to the log file specified during Logger instance initialization.
350 |
351 | This method appends the provided message to the log file specified by the `logfile` attribute. If no log file path is specified, this method does nothing.
352 |
353 | Args:
354 | message (str): The message to be written to the log file.
355 | """
356 |
357 | if self.logfile is not None:
358 | f = open(self.logfile, "a")
359 | nocolor_message = re.sub(r"\x1b[\[]([0-9;]+)m", "", message)
360 | f.write(nocolor_message + end)
361 | f.close()
362 |
363 |
364 | class MicrosoftDNS(object):
365 | """
366 | Class to interact with Microsoft DNS servers for resolving domain names to IP addresses.
367 |
368 | Attributes:
369 | dnsserver (str): The IP address of the DNS server.
370 | verbose (bool): Flag to enable verbose mode.
371 | auth_domain (str): The authentication domain.
372 | auth_username (str): The authentication username.
373 | auth_password (str): The authentication password.
374 | auth_dc_ip (str): The IP address of the domain controller.
375 | auth_lm_hash (str): The LM hash for authentication.
376 | auth_nt_hash (str): The NT hash for authentication.
377 | """
378 |
379 | __wildcard_dns_cache = {}
380 |
381 | def __init__(self, dnsserver, auth_domain, auth_username, auth_password, auth_dc_ip, auth_lm_hash, auth_nt_hash, use_ldaps=False, logger=None):
382 | super(MicrosoftDNS, self).__init__()
383 | self.dnsserver = dnsserver
384 |
385 | self.auth_domain = auth_domain
386 | self.auth_username = auth_username
387 | self.auth_password = auth_password
388 | self.auth_dc_ip = auth_dc_ip
389 | self.auth_lm_hash = auth_lm_hash
390 | self.auth_nt_hash = auth_nt_hash
391 | self.use_ldaps = use_ldaps
392 |
393 | self.logger = logger
394 |
395 | def resolve(self, target_name):
396 | """
397 | Resolves a given target name to its corresponding IP addresses.
398 |
399 | This method attempts to resolve the provided target name to its IP addresses using both A and AAAA record types. It iterates through the DNS answers for each record type, appending the addresses to a list. If no records are found, it logs a debug message indicating the absence of records for the target name.
400 |
401 | Args:
402 | target_name (str): The target name to be resolved to IP addresses.
403 |
404 | Returns:
405 | list: A list of IP addresses corresponding to the target name.
406 | """
407 |
408 | target_ips = []
409 | for rdtype in ["A", "AAAA"]:
410 | dns_answer = self.getRecord(value=target_name, rdtype=rdtype)
411 | if dns_answer is not None:
412 | for record in dns_answer:
413 | target_ips.append(record.address)
414 |
415 | if len(target_ips) == 0:
416 | self.logger.debug("[MicrosoftDNS] No records found for %s." % target_name)
417 |
418 | return target_ips
419 |
420 | def reverseLookup(self, target_ip, use_tcp=False):
421 | """
422 | Performs a reverse DNS lookup for a given IP address.
423 |
424 | This method attempts to perform a reverse DNS lookup for the provided IP address using the PTR record type. It can use either UDP or TCP protocol for the lookup, depending on the specified option. If the lookup is successful, it returns the hostname associated with the IP address. If the lookup fails or no record is found, it logs a debug message indicating the failure or absence of records.
425 |
426 | Args:
427 | target_ip (str): The IP address for which to perform the reverse DNS lookup.
428 | use_tcp (bool, optional): Indicates whether to use TCP protocol for the lookup. Defaults to False.
429 |
430 | Returns:
431 | str: The hostname associated with the IP address if the lookup is successful, otherwise the original IP address.
432 | """
433 |
434 | result = target_ip
435 | try:
436 | addr = dns.reversename.from_address(target_ip)
437 | except dns.exception.SyntaxError:
438 | return None
439 | else:
440 | try:
441 | answer = str(dns.resolver.resolve(addr, 'PTR', tcp=use_tcp)[0])
442 | result = answer.rstrip('.')
443 | except (dns.resolver.NXDOMAIN, dns.resolver.Timeout) as e:
444 | pass
445 | except Exception as e:
446 | self.logger.debug("[MicrosoftDNS] DNS lookup failed: %s" % addr)
447 | pass
448 |
449 | return result.upper()
450 |
451 | def getRecord(self, rdtype, value):
452 | """
453 | Retrieves DNS records for a specified value and record type using UDP and TCP protocols.
454 |
455 | Parameters:
456 | rdtype (str): The type of DNS record to retrieve.
457 | value (str): The value for which the DNS record is to be retrieved.
458 |
459 | Returns:
460 | dns.resolver.Answer: The DNS answer containing the resolved records.
461 |
462 | Raises:
463 | dns.resolver.NXDOMAIN: If the domain does not exist.
464 | dns.resolver.NoAnswer: If the domain exists but does not have the specified record type.
465 | dns.resolver.NoNameservers: If no nameservers are found for the domain.
466 | dns.exception.DNSException: For any other DNS-related exceptions.
467 | """
468 |
469 | dns_resolver = dns.resolver.Resolver()
470 | dns_resolver.nameservers = [self.dnsserver]
471 | dns_answer = None
472 |
473 | # Try UDP
474 | try:
475 | dns_answer = dns_resolver.resolve(value, rdtype=rdtype, tcp=False)
476 | except dns.resolver.NXDOMAIN:
477 | # the domain does not exist so dns resolutions remain empty
478 | pass
479 | except dns.resolver.NoAnswer as e:
480 | # domains existing but not having AAAA records is common
481 | pass
482 | except dns.resolver.NoNameservers as e:
483 | pass
484 | except dns.exception.DNSException as e:
485 | pass
486 |
487 | if dns_answer is None:
488 | # Try TCP
489 | try:
490 | dns_answer = dns_resolver.resolve(value, rdtype=rdtype, tcp=True)
491 | except dns.resolver.NXDOMAIN:
492 | # the domain does not exist so dns resolutions remain empty
493 | pass
494 | except dns.resolver.NoAnswer as e:
495 | # domains existing but not having AAAA records is common
496 | pass
497 | except dns.resolver.NoNameservers as e:
498 | pass
499 | except dns.exception.DNSException as e:
500 | pass
501 |
502 | return dns_answer
503 |
504 | def checkPresenceOfWildcardDns(self):
505 | """
506 | Check the presence of wildcard DNS entries in the Microsoft DNS server.
507 |
508 | This function queries the Microsoft DNS server to find wildcard DNS entries in the DomainDnsZones of the specified domain.
509 | It retrieves information about wildcard DNS entries and prints a warning message if any are found.
510 |
511 | Returns:
512 | dict: A dictionary containing information about wildcard DNS entries found in the Microsoft DNS server.
513 | """
514 |
515 | ldap_server, ldap_session = init_ldap_session(
516 | auth_domain=self.auth_domain,
517 | auth_dc_ip=self.auth_dc_ip,
518 | auth_username=self.auth_username,
519 | auth_password=self.auth_password,
520 | auth_lm_hash=self.auth_lm_hash,
521 | auth_nt_hash=self.auth_nt_hash,
522 | use_ldaps=self.use_ldaps
523 | )
524 |
525 | target_dn = "CN=MicrosoftDNS,DC=DomainDnsZones," + ldap_server.info.other["rootDomainNamingContext"][0]
526 |
527 | ldapresults = list(ldap_session.extend.standard.paged_search(target_dn, "(&(objectClass=dnsNode)(dc=\\2A))", attributes=["distinguishedName", "dNSTombstoned"]))
528 |
529 | results = {}
530 | for entry in ldapresults:
531 | if entry['type'] != 'searchResEntry':
532 | continue
533 | results[entry['dn']] = entry["attributes"]
534 |
535 | if len(results.keys()) != 0:
536 | self.logger.dateprint("[!] WARNING! Wildcard DNS entries found, dns resolution will not be consistent.")
537 | for dn, data in results.items():
538 | fqdn = re.sub(','+target_dn+'$', '', dn)
539 | fqdn = '.'.join([dc.split('=')[1] for dc in fqdn.split(',')])
540 |
541 | ips = self.resolve(fqdn)
542 |
543 | if data["dNSTombstoned"]:
544 | self.logger.dateprint(" | %s ──> %s (set to be removed)" % (dn, ips))
545 | else:
546 | self.logger.dateprint(" | %s ──> %s" % (dn, ips))
547 |
548 | # Cache found wildcard dns
549 | for ip in ips:
550 | if fqdn not in self.__wildcard_dns_cache.keys():
551 | self.__wildcard_dns_cache[fqdn] = {}
552 | if ip not in self.__wildcard_dns_cache[fqdn].keys():
553 | self.__wildcard_dns_cache[fqdn][ip] = []
554 | self.__wildcard_dns_cache[fqdn][ip].append(data)
555 | print()
556 | return results
557 |
558 |
559 | class RPCSessionsEnumerator(object):
560 | """
561 | RPCSessionsEnumerator is a class designed to enumerate RPC sessions on a target system. It facilitates the establishment of a DCE RPC connection to the target system, allowing for the enumeration of RPC sessions. This class is particularly useful for reconnaissance and discovery of RPC services running on a target system.
562 |
563 | Attributes:
564 | options (dict): A dictionary containing options for the enumeration process.
565 | address (str): The IP address of the target system.
566 | hostname (str): The hostname of the target system.
567 | username (str): The username to use for authentication.
568 | domain (str): The domain to use for authentication.
569 | password (str): The password to use for authentication.
570 | lmhash (str): The LM hash of the password to use for authentication.
571 | nthash (str): The NT hash of the password to use for authentication.
572 | aesKey (str): The AES key to use for authentication.
573 | smbconnection (object): The SMB connection object.
574 | """
575 |
576 | def __init__(self, options, logger, midns, hostname, address, username, domain, password, lmhash='', nthash='', aesKey=None):
577 | self.options = options
578 | self.midns = midns
579 | self.logger = logger
580 | # Target
581 | self.address = address
582 | self.hostname = hostname
583 | # Credentials
584 | self.username = username
585 | self.password = password
586 | self.domain = domain
587 | self.lmhash = lmhash
588 | self.nthash = nthash
589 | self.aesKey = aesKey
590 |
591 | self.smbconnection = None
592 |
593 | def dce_rpc_connect(self, binding, uuid):
594 | """
595 | Establishes a DCE RPC connection to the target system.
596 |
597 | This method sets up a DCE RPC connection to the target system using the provided binding and UUID. It configures the connection with the target's hostname and address, sets the credentials for authentication, and attempts to connect. If the connection is successful, it returns the DCE RPC object. If the connection fails due to hostname validation or other issues, it logs the error and returns None.
598 |
599 | Parameters:
600 | - binding (str): The binding string for the RPC connection.
601 | - uuid (str): The UUID of the RPC service to connect to.
602 |
603 | Returns:
604 | - dce (DCERPCTransportFactory): The DCE RPC object if the connection is successful, otherwise None.
605 | """
606 |
607 | try:
608 | self.rpc = transport.DCERPCTransportFactory(binding)
609 | self.rpc.set_connect_timeout(1.0)
610 |
611 | # Set name/host explicitly
612 | self.rpc.setRemoteName(self.hostname)
613 | self.rpc.setRemoteHost(self.address)
614 |
615 | # Else set the required stuff for NTLM
616 | if hasattr(self.rpc, "set_credentials"):
617 | self.rpc.set_credentials(
618 | self.username,
619 | self.password,
620 | domain=self.domain,
621 | lmhash=self.lmhash,
622 | nthash=self.nthash
623 | )
624 |
625 | dce = self.rpc.get_dce_rpc()
626 |
627 | # Try connecting, catch hostname validation
628 | try:
629 | dce.connect()
630 | except (SMB3.HostnameValidationException, SMB.HostnameValidationException) as exc:
631 | self.logger.info("Ignoring host %s since its hostname does not match: %s" % (self.hostname, str(exc)))
632 | return None
633 | except SessionError as exc:
634 | if ("STATUS_PIPE_NOT_AVAILABLE" in str(exc) or "STATUS_OBJECT_NAME_NOT_FOUND" in str(exc)) and "winreg" in binding.lower():
635 | # This can happen, silently ignore
636 | return None
637 | if "STATUS_MORE_PROCESSING_REQUIRED" in str(exc):
638 | try:
639 | self.rpc.get_smb_connection().close()
640 | except:
641 | pass
642 | # Try again!
643 | return self.dce_rpc_connect(binding, uuid, False)
644 | # Else, just log it
645 | if self.options.debug:
646 | traceback.print_exc()
647 | self.logger.error("DCE/RPC connection failed: %s" % str(exc))
648 | return None
649 |
650 | if self.smbconnection is None:
651 | self.smbconnection = self.rpc.get_smb_connection()
652 | # We explicity set the smbconnection back to the rpc object
653 | # this way it won"t be closed when we call disconnect()
654 | self.rpc.set_smb_connection(self.smbconnection)
655 |
656 | dce.bind(uuid)
657 |
658 | except DCERPCException as e:
659 | if self.options.debug:
660 | traceback.print_exc()
661 | self.logger.error("DCE/RPC connection failed: %s" % str(e))
662 | return None
663 | except KeyboardInterrupt:
664 | raise
665 | except Exception as err:
666 | if self.options.debug:
667 | traceback.print_exc()
668 | self.logger.error("DCE/RPC connection failed: %s" % str(err))
669 | return None
670 |
671 | return dce
672 |
673 | def rpc_get_sessions(self):
674 | """
675 | This function retrieves a list of active sessions on the target system.
676 |
677 | It establishes a DCE/RPC connection to the target system using the srvs.MSRPC_UUID_SRVS UUID, which is used for session enumeration.
678 | It then calls the hNetrSessionEnum function to enumerate the active sessions on the target system.
679 | The function filters out the current session of the user running the script and machine accounts.
680 | It computes the session time and idle time for each session and returns a list of sessions with their details.
681 |
682 | Returns:
683 | list: A list of active sessions on the target system, each session represented as a dictionary containing the username, session time, and idle time.
684 | """
685 |
686 | dce = self.dce_rpc_connect(r"ncacn_np:%s[\PIPE\srvsvc]" % self.address, srvs.MSRPC_UUID_SRVS)
687 |
688 | if dce is None:
689 | return []
690 |
691 | try:
692 | resp = srvs.hNetrSessionEnum(dce, "\x00", NULL, 10)
693 | except DCERPCException as err:
694 | # impacket.dcerpc.v5.rpcrt.DCERPCException: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied
695 | if "rpc_s_access_denied" in str(err):
696 | if self.options.debug:
697 | self.logger.debug("Access denied while enumerating Sessions on %s, likely a patched OS" % self.hostname)
698 | self.logger.error("Error: %s" % str(err))
699 | return []
700 | else:
701 | raise
702 | except Exception as err:
703 | if str(err).find("Broken pipe") >= 0:
704 | if self.options.debug:
705 | self.logger.error("Error: %s" % str(err))
706 | return []
707 | else:
708 | raise
709 |
710 | sessions = []
711 | for session in resp["InfoStruct"]["SessionInfo"]["Level10"]["Buffer"]:
712 | # Compute sessionTime
713 | sessionTime = datetime.datetime.now()
714 | # I am removing microseconds here to avoid useless remainders
715 | sessionTime -= datetime.timedelta(microseconds=sessionTime.microsecond)
716 | sessionTime -= datetime.timedelta(seconds=session["sesi10_time"])
717 |
718 | # Parse sessionIdleTime (IDLE since N seconds)
719 | sessionIdleTime = datetime.timedelta(seconds=session["sesi10_idle_time"])
720 |
721 | # Parse the username of the connection
722 | sessionUsername = session["sesi10_username"][:-1]
723 | # Skip our connection
724 | if sessionUsername == self.username:
725 | continue
726 | # Skip empty usernames
727 | if len(sessionUsername) == 0:
728 | continue
729 | # Skip machine accounts
730 | if sessionUsername[-1] == "$":
731 | continue
732 |
733 | # Parse the source of the connection
734 | sessionSource = session["sesi10_cname"][:-1].upper()
735 | # Strip \\ from IPs
736 | if sessionSource[:2] == "\\\\":
737 | sessionSource = sessionSource[2:]
738 | # Skip empty IPs
739 | if sessionSource == "":
740 | continue
741 | # Skip local connections
742 | if sessionSource in ["127.0.0.1", "[::1]"]:
743 | continue
744 | # IPv6 address
745 | if sessionSource[0] == "[" and sessionSource[-1] == "]":
746 | sessionSource = sessionSource[1:-1]
747 |
748 | if is_ipv4_addr(sessionSource):
749 | machineFqdn = self.midns.reverseLookup(sessionSource)
750 | if machineFqdn is not None:
751 | sessionSource = machineFqdn
752 |
753 | sessions.append({
754 | "username": sessionUsername,
755 | "source": sessionSource,
756 | "target": self.hostname.upper(),
757 | "sessionTime": sessionTime,
758 | "sessionIdleTime": sessionIdleTime
759 | })
760 |
761 | dce.disconnect()
762 |
763 | return sessions
764 |
765 |
766 | def worker(options, target, domain, username, password, lmhash, nthash, registered_domains, midns, logger, results, lock):
767 | """
768 | This function is a worker that processes a target for unusual sessions. It takes in options, target information, domain, username, password, LM and NT hashes, registered domains, and a midns object. It attempts to resolve the target IP, checks if port 445 is open, and if so, enumerates RPC sessions. It then prints out information about any unusual sessions found.
769 |
770 | Parameters:
771 | - options: A dictionary of options for the script.
772 | - target: A tuple containing the type and data of the target.
773 | - domain: The domain to use for authentication.
774 | - username: The username to use for authentication.
775 | - password: The password to use for authentication.
776 | - lmhash: The LM hash to use for authentication.
777 | - nthash: The NT hash to use for authentication.
778 | - registered_domains: A list of registered domains.
779 | - midns: An object for DNS resolution.
780 | - results: A dictionary to store results.
781 | - lock: A lock object for thread-safe printing.
782 | """
783 |
784 | try:
785 | target_type, target_data = target
786 |
787 | target_ips = None
788 | target_name = ""
789 | if target_type.lower() in ["ip", "ipv4", "ipv6"]:
790 | target_name = target_data
791 | target_ips = [target_data]
792 |
793 | elif target_type.lower() in ["fqdn"]:
794 | target_name = target_data
795 | target_ips = midns.resolve(target_data)
796 | if target_ips is not None:
797 | if options.debug:
798 | lock.acquire()
799 | logger.debug("[+] Resolved '%s' to %s" % (target_name, target_ips))
800 | lock.release()
801 |
802 | if target_ips is not None:
803 | if len(target_ips) == 0:
804 | if options.debug:
805 | logger.debug("No IP addresses found for %s" % target_name)
806 | return
807 |
808 | if is_port_open(target=target_ips[0], port=445):
809 | rpc_sessions_enumerator = RPCSessionsEnumerator(
810 | options=options,
811 | logger=logger,
812 | midns=midns,
813 | hostname=target_name,
814 | address=target_ips[0],
815 | username=username,
816 | domain=domain,
817 | password=password,
818 | lmhash=lmhash,
819 | nthash=nthash
820 | )
821 |
822 | sessions = rpc_sessions_enumerator.rpc_get_sessions()
823 |
824 | if len(sessions) != 0:
825 | results["target_name"] = []
826 | for session in sessions:
827 | if is_ipv4_addr(session["source"]):
828 | if not options.ignore_unknown:
829 | logger.dateprint(f"[\x1b[1;48;2;255;171;0;97m Unknown \x1b[0m] User \x1b[1;93m%s\x1b[0m is logged in on \x1b[1;93m%s\x1b[0m at \x1b[1;94m%s\x1b[0m from \x1b[1;95m%s\x1b[0m, IDLE since \x1b[1;96m%s\x1b[0m" % (
830 | session["username"],
831 | session["target"],
832 | session["sessionTime"],
833 | session["source"],
834 | timedeltaStr(session["sessionIdleTime"])
835 | ))
836 |
837 | elif is_fqdn(session["source"]):
838 | source_domain = session["source"].split('.', 1)[-1].upper()
839 | if source_domain not in registered_domains:
840 | logger.dateprint(f"[\x1b[1;48;2;233;61;3;97mExt. Domain\x1b[0m] User \x1b[1;93m%s\x1b[0m is logged in on \x1b[1;93m%s\x1b[0m at \x1b[1;94m%s\x1b[0m from %s, IDLE since \x1b[1;96m%s\x1b[0m" % (
841 | session["username"],
842 | session["target"],
843 | session["sessionTime"],
844 | "\x1b[1;95m%s\x1b[0m.\x1b[1;48;2;233;61;3;97m%s\x1b[0m" % (session["source"].split('.', 1)[0], session["source"].split('.', 1)[1]),
845 | timedeltaStr(session["sessionIdleTime"])
846 | ))
847 | else:
848 | if options.verbose or options.debug:
849 | logger.dateprint(f"[\x1b[1;48;2;83;170;51;97m Legit \x1b[0m] User \x1b[1;93m%s\x1b[0m is logged in on \x1b[1;93m%s\x1b[0m at \x1b[1;94m%s\x1b[0m from \x1b[1;95m%s\x1b[0m, IDLE since \x1b[1;96m%s\x1b[0m" % (
850 | session["username"],
851 | session["target"],
852 | session["sessionTime"],
853 | session["source"],
854 | timedeltaStr(session["sessionIdleTime"])
855 | ))
856 | else:
857 | if options.verbose or options.debug:
858 | logger.dateprint(f"[\x1b[1;48;2;83;170;51;97m Legit \x1b[0m] User \x1b[1;93m%s\x1b[0m is logged in on \x1b[1;93m%s\x1b[0m at \x1b[1;94m%s\x1b[0m from \x1b[1;95m%s\x1b[0m, IDLE since \x1b[1;96m%s\x1b[0m" % (
859 | session["username"],
860 | session["target"],
861 | session["sessionTime"],
862 | session["source"],
863 | timedeltaStr(session["sessionIdleTime"])
864 | ))
865 | results["target_name"].append(session)
866 | else:
867 | logger.debug("Port 445 is closed on %s (%s)" % (target_name, target_ips))
868 |
869 | except Exception as e:
870 | if options.debug:
871 | traceback.print_exc()
872 |
873 |
874 | def parseArgs():
875 | print("FindUnusualSessions v%s - by Remi GASCOU (Podalirius)\n" % VERSION)
876 |
877 | parser = argparse.ArgumentParser(add_help=True, description="")
878 |
879 | parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode. (default: False).")
880 | parser.add_argument("--debug", dest="debug", action="store_true", default=False, help="Debug mode. (default: False).")
881 | parser.add_argument("--no-colors", dest="no_colors", action="store_true", default=False, help="Disables colored output mode.")
882 | parser.add_argument("-L", "--logfile", dest="logfile", metavar="LOGFILE", required=False, default=None, type=str, help="File to write logs to.")
883 | parser.add_argument("-t", "--threads", dest="threads", action="store", type=int, default=64, required=False, help="Number of threads (default: 64).")
884 | parser.add_argument("-ns", "--nameserver", dest="nameserver", default=None, required=False, help="IP of the DNS server to use, instead of the --dc-ip.")
885 | parser.add_argument("-ld", "--legitimate-domains", dest="legitimate_domains", default=[], required=False, action="append", help="Legitimate domains to use for comparison.")
886 |
887 | group_targets_source = parser.add_argument_group("Targets")
888 | group_targets_source.add_argument("-tf", "--targets-file", default=None, type=str, help="Path to file containing a line by line list of targets.")
889 | group_targets_source.add_argument("-tt", "--target", default=[], type=str, action='append', help="Target IP, FQDN or CIDR.")
890 | group_targets_source.add_argument("-ad", "--auth-domain", default="", type=str, help="Windows domain to authenticate to.")
891 | group_targets_source.add_argument("-ai", "--auth-dc-ip", default=None, type=str, help="IP of the domain controller.")
892 | group_targets_source.add_argument("-au", "--auth-user", default=None, type=str, help="Username of the domain account.")
893 | group_targets_source.add_argument("--ldaps", default=False, action="store_true", help="Use LDAPS (default: False)")
894 | group_targets_source.add_argument("--no-ldap", default=False, action="store_true", help="Do not perform LDAP queries.")
895 | group_targets_source.add_argument("--subnets", default=False, action="store_true", help="Get all subnets from the domain and use them as targets (default: False)")
896 | group_targets_source.add_argument("-tl", "--target-ldap-query", dest="target_ldap_query", type=str, default=None, required=False, help="LDAP query to use to extract computers from the domain.")
897 | group_targets_source.add_argument("--ignore-unknown", default=False, action="store_true", help="Ignore unknown sources (default: False)")
898 |
899 | secret = parser.add_argument_group("Credentials")
900 | cred = secret.add_mutually_exclusive_group()
901 | cred.add_argument("--no-pass", default=False, action="store_true", help="Don't ask for password (useful for -k)")
902 | cred.add_argument("-ap", "--auth-password", default=None, type=str, help="Password of the domain account.")
903 | cred.add_argument("-ah", "--auth-hashes", default=None, type=str, help="LM:NT hashes to pass the hash for this user.")
904 | cred.add_argument("--aes-key", dest="auth_key", action="store", metavar="hex key", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
905 | secret.add_argument("-k", "--kerberos", dest="auth_use_kerberos", action="store_true", help="Use Kerberos authentication. Grabs credentials from .ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line")
906 | secret.add_argument("--kdcHost", dest="auth_kdcHost", default=None, type=str, help="IP of the domain controller.")
907 |
908 | output = parser.add_argument_group("Output files")
909 | output.add_argument("--export-xlsx", dest="export_xlsx", type=str, default=None, required=False, help="Output XLSX file to store the results in.")
910 | output.add_argument("--export-json", dest="export_json", type=str, default=None, required=False, help="Output JSON file to store the results in.")
911 | output.add_argument("--export-sqlite", dest="export_sqlite", type=str, default=None, required=False, help="Output SQLITE3 file to store the results in.")
912 |
913 | if len(sys.argv) == 1:
914 | parser.print_help()
915 | sys.exit(1)
916 |
917 | options = parser.parse_args()
918 |
919 | if options.auth_password is None and options.no_pass == False and options.auth_hashes is None:
920 | print("[+] No password or hashes provided and --no-pass is '%s'" % options.no_pass)
921 | from getpass import getpass
922 | if options.auth_domain is not None:
923 | options.auth_password = getpass(" | Provide a password for '%s\\%s':" % (options.auth_domain, options.auth_user))
924 | else:
925 | options.auth_password = getpass(" | Provide a password for '%s':" % options.auth_user)
926 |
927 | return options
928 |
929 |
930 | if __name__ == '__main__':
931 | options = parseArgs()
932 |
933 | loglevel = LogLevel.INFO
934 | if options.verbose == True:
935 | loglevel = LogLevel.VERBOSE
936 | if options.debug == True:
937 | loglevel = LogLevel.DEBUG
938 | logger = Logger(loglevel=loglevel, logfile=options.logfile, no_colors=options.no_colors)
939 |
940 | # Parse hashes
941 | if options.auth_hashes is not None:
942 | if ":" not in options.auth_hashes:
943 | options.auth_hashes = ":" + options.auth_hashes
944 | auth_lm_hash, auth_nt_hash = parse_lm_nt_hashes(options.auth_hashes)
945 |
946 | # Use AES Authentication key if available
947 | if options.auth_key is not None:
948 | options.auth_use_kerberos = True
949 | if options.auth_use_kerberos is True and options.auth_kdcHost is None:
950 | logger.warn("Specify KDC's Hostname of FQDN using the argument --kdcHost")
951 | exit()
952 |
953 | try:
954 | if options.auth_dc_ip is not None and options.auth_user is not None and (options.auth_password is not None or options.auth_hashes is not None):
955 | midns = MicrosoftDNS(
956 | dnsserver=options.nameserver,
957 | auth_domain=options.auth_domain,
958 | auth_username=options.auth_user,
959 | auth_password=options.auth_password,
960 | auth_dc_ip=options.auth_dc_ip,
961 | auth_lm_hash=auth_lm_hash,
962 | auth_nt_hash=auth_nt_hash,
963 | use_ldaps=options.ldaps,
964 | logger=logger
965 | )
966 | midns.checkPresenceOfWildcardDns()
967 |
968 | if options.debug:
969 | logger.debug("[>] Parsing targets ...")
970 | sys.stdout.flush()
971 |
972 | registered_domains = getDomainsAndTrusts(
973 | auth_domain=options.auth_domain,
974 | auth_username=options.auth_user,
975 | auth_password=options.auth_password,
976 | auth_dc_ip=options.auth_dc_ip,
977 | auth_lm_hash=auth_lm_hash,
978 | auth_nt_hash=auth_nt_hash,
979 | use_ldaps=options.ldaps,
980 | )
981 | if options.legitimate_domains:
982 | registered_domains.extend(options.legitimate_domains)
983 |
984 | if options.debug:
985 | logger.debug("[>] Legitimate domains: %s" % registered_domains)
986 |
987 | logger.info("[>] Loading targets ...")
988 | targets = load_targets(options, logger)
989 | logger.info("[+] Found %d targets." % len(targets))
990 |
991 | logger.info("[>] Enumerating logged in users on each computer ...")
992 | results = {}
993 | if len(targets) != 0:
994 | # Setup thread lock to properly write in the file
995 | lock = threading.Lock()
996 | # Waits for all the threads to be completed
997 | with ThreadPoolExecutor(max_workers=min(options.threads, len(targets))) as tp:
998 | for target in targets:
999 | tp.submit(
1000 | worker,
1001 | options,
1002 | target,
1003 | options.auth_domain,
1004 | options.auth_user,
1005 | options.auth_password,
1006 | auth_lm_hash,
1007 | auth_nt_hash,
1008 | registered_domains,
1009 | midns,
1010 | logger,
1011 | results,
1012 | lock
1013 | )
1014 |
1015 | if options.export_json is not None:
1016 | export_json(options, results)
1017 |
1018 | if options.export_xlsx is not None:
1019 | export_xlsx(options, results)
1020 |
1021 | if options.export_sqlite is not None:
1022 | export_sqlite(options, results)
1023 | else:
1024 | logger.warn("No computers parsed from the targets.")
1025 | logger.info("[+] Bye Bye!")
1026 |
1027 | except Exception as e:
1028 | if options.debug:
1029 | traceback.print_exc()
1030 | logger.warn("Error: %s" % str(e))
1031 |
1032 |
--------------------------------------------------------------------------------