diff --git a/README.md b/README.md index 730999b..9b6003a 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,32 @@ # ![bloodyAD logo](https://repository-images.githubusercontent.com/415977068/9b2fed72-35fb-4faa-a8d3-b120cd3c396f) autobloody -autobloody is a tool to automatically exploit Active Directory privilege escalation paths shown by BloodHound combining `pathgen.py` and `autobloody.py`. +autobloody is a tool to automatically exploit Active Directory privilege escalation paths shown by BloodHound combining `pathgen` and `autobloody`. ## Description This tool automates the AD privesc between two AD objects, the source (the one we own) and the target (the one we want) if a privesc path exists in BloodHound database. The automation is split in two parts in order to be used transparently with tunneling tools such as proxychains: -- `pathgen.py` to find the optimal path for privesc using bloodhound data and neo4j queries. -- `autobloody.py` to execute the path found with `pathgen.py` +- `pathgen` to find the optimal path for privesc using bloodhound data and neo4j queries. +- `autobloody` to execute the path found with `pathgen` autobloody relies on [bloodyAD](https://github.com/CravateRouge/bloodyAD) and supports authentication using cleartext passwords, pass-the-hash, pass-the-ticket or certificates and binds to LDAP services of a domain controller to perform AD privesc. -## Requirements -The following are required: +## Installation +A python package is available: +```ps1 +pip install autobloody +``` + +Or you can clone the repo: +```ps1 +git clone --depth 1 https://github.com/CravateRouge/autobloody +pip install . +``` +### Dependencies - [bloodyAD](https://github.com/CravateRouge/bloodyAD) - Neo4j python driver - Neo4j with the [GDS library](https://neo4j.com/docs/graph-data-science/current/installation/) - BloodHound - Python 3 -Use the requirements.txt for your virtual environment: `pip3 install -r requirements.txt` - ## How to use it First data must be imported into BloodHound (e.g using SharpHound or BloodHound.py) and Neo4j must be running. @@ -26,13 +34,13 @@ First data must be imported into BloodHound (e.g using SharpHound or BloodHound. Simple usage: ```ps1 -pathgen.py -dp neo4jPass -ds 'OWNED_USER@ATTACK.LOCAL' -dt 'TARGET_USER@ATTACK.LOCAL' && proxychains autobloody.py -d ATTACK -u 'owned_user' -p 'owned_user_pass' --host dc01.attack.local +pathgen -dp neo4jPass -ds 'OWNED_USER@ATTACK.LOCAL' -dt 'TARGET_USER@ATTACK.LOCAL' && proxychains autobloody.py -d ATTACK -u 'owned_user' -p 'owned_user_pass' --host dc01.attack.local ``` -Full help for `pathgen.py`: +Full help for `pathgen`: ```ps1 -[bloodyAD]$ python pathgen.py -h -usage: pathgen.py [-h] [--dburi DBURI] [-du DBUSER] -dp DBPASSWORD -ds DBSOURCE -dt DBTARGET [-f FILEPATH] +[bloodyAD]$ pathgen -h +usage: pathgen [-h] [--dburi DBURI] [-du DBUSER] -dp DBPASSWORD -ds DBSOURCE -dt DBTARGET [-f FILEPATH] Attack Path Generator @@ -51,10 +59,10 @@ options: File path for the graph path file (default is "path.json") ``` -Full help for `autobloody.py`: +Full help for `autobloody`: ```ps1 -[bloodyAD]$ python autobloody.py -h -usage: autobloody.py [-h] [-d DOMAIN] [-u USERNAME] [-p PASSWORD] [-k] [-s] --host HOST [--path PATH] +[bloodyAD]$ autobloody -h +usage: autobloody [-h] [-d DOMAIN] [-u USERNAME] [-p PASSWORD] [-k] [-s] --host HOST [--path PATH] Attack Path Executor @@ -75,13 +83,13 @@ options: ``` ## How it works -First `pathgen.py` generates a privesc path using the Dijkstra's algorithm implemented into the Neo4j's GDS library. +First `pathgen` generates a privesc path using the Dijkstra's algorithm implemented into the Neo4j's GDS library. The Dijkstra's algorithm allows to solve the shortest path problem on a weighted graph. By default the edges created by BloodHound don't have weight but a type (e.g MemberOf, WriteOwner). A weight is then added to each edge accordingly to the type of edge and the type of node reached (e.g user,group,domain). -Once a path is generated and stored as a json file, `autobloody.py` will connect to the DC and execute the path and clean what is reversible (everything except password change). +Once a path is generated and stored as a json file, `autobloody` will connect to the DC and execute the path and clean what is reversible (everything except password change). ## Limitations -Here is the list of the BloodHound edges currently supported for automatic exploitation: +For now, only the following BloodHound edges are currently supported for automatic exploitation: - MemberOf - ForceChangePassword - AddMembers diff --git a/autobloody.py b/autobloody.py index 9aa6e15..5a03f5d 100755 --- a/autobloody.py +++ b/autobloody.py @@ -1,30 +1,6 @@ #!/usr/bin/env python3 -import argparse, json, sys -from autobloody import automation - -def main(): - parser = argparse.ArgumentParser(description='Attack Path Executor', formatter_class=argparse.RawTextHelpFormatter) - - # Exploitation parameters - parser.add_argument('-d', '--domain', help='Domain used for NTLM authentication') - parser.add_argument('-u', '--username', help='Username used for NTLM authentication') - parser.add_argument('-p', '--password', help='Cleartext password or LMHASH:NTHASH for NTLM authentication') - parser.add_argument('-k', '--kerberos', action='store_true', default=False) - parser.add_argument('-c', '--certificate', help='Certificate authentication, e.g: "path/to/key:path/to/cert"') - parser.add_argument('-s', '--secure', help='Try to use LDAP over TLS aka LDAPS (default is LDAP)', action='store_true', default=False) - parser.add_argument('--host', help='Hostname or IP of the DC (ex: my.dc.local or 172.16.1.3)', required=True) - parser.add_argument('--path', help='Filename of the attack path generated with pathgen.py (default is "path.json")', default="path.json") - - if len(sys.argv)==1: - parser.print_help(sys.stderr) - sys.exit(1) - - args = parser.parse_args() - automate = automation.Automation(args) - with open(args.path, 'r') as f: - automate.exploit(json.load(f)) - print("[+] Done, attack path executed") +from autobloody import main if __name__ == '__main__': - main() + main.main() diff --git a/autobloody/database.py b/autobloody/database.py index 4ce6900..3f53aee 100644 --- a/autobloody/database.py +++ b/autobloody/database.py @@ -1,8 +1,10 @@ from neo4j import GraphDatabase +import logging class Database: def __init__(self, uri, user, password): + logging.getLogger("neo4j").setLevel(logging.WARNING) self.driver = GraphDatabase.driver(uri, auth=(user, password)) self._prepareDb() diff --git a/autobloody/main.py b/autobloody/main.py new file mode 100755 index 0000000..7d569a0 --- /dev/null +++ b/autobloody/main.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +import argparse, json, sys +from autobloody import automation, database, proxy_bypass + +def main(): + parser = argparse.ArgumentParser(description='AD Privesc Automation', formatter_class=argparse.RawTextHelpFormatter) + + # DB parameters + parser.add_argument("--dburi", default="bolt://localhost:7687", help="The host neo4j is running on (default is \"bolt://localhost:7687\")") + parser.add_argument("-du", "--dbuser", default="neo4j", help="Neo4j username to use (default is \"neo4j\")") + parser.add_argument("-dp", "--dbpassword", help="Neo4j password to use", required=True) + parser.add_argument("-ds", "--dbsource", help="Case sensitive label of the source node (name property in bloodhound)", required=True) + parser.add_argument("-dt", "--dbtarget", help="Case sensitive label of the target node (name property in bloodhound)", required=True) + + # Exploitation parameters + parser.add_argument('-d', '--domain', help='Domain used for NTLM authentication') + parser.add_argument('-u', '--username', help='Username used for NTLM authentication') + parser.add_argument('-p', '--password', help='Cleartext password or LMHASH:NTHASH for NTLM authentication') + parser.add_argument('-k', '--kerberos', action='store_true', default=False) + parser.add_argument('-c', '--certificate', help='Certificate authentication, e.g: "path/to/key:path/to/cert"') + parser.add_argument('-s', '--secure', help='Try to use LDAP over TLS aka LDAPS (default is LDAP)', action='store_true', default=False) + parser.add_argument('--host', help='Hostname or IP of the DC (ex: my.dc.local or 172.16.1.3)', required=True) + + if len(sys.argv)==1: + parser.print_help(sys.stderr) + sys.exit(1) + + args = parser.parse_args() + + path_dict = pathgen(args) + + automate = automation.Automation(args) + automate.exploit(path_dict) + print("[+] Done, attack path executed") + + +def pathgen(args): + bypass = proxy_bypass.ProxyBypass() + db = database.Database(args.dburi, args.dbuser, args.dbpassword) + + path = db.getPrivescPath(args.dbsource, args.dbtarget) + path_dict = [] + for rel in path: + start_node = {'name':rel.start_node['name'], 'distinguishedname':rel.start_node['distinguishedname'], 'objectid':rel.start_node['objectid']} + end_node = {'name':rel.end_node['name'], 'distinguishedname':rel.end_node['distinguishedname'], 'objectid': rel.end_node['objectid']} + path_dict.append({'start_node':start_node, 'end_node':end_node, 'cost':rel['cost']}) + + db.close() + bypass.disable() + + print(f"[+] Done, {len(path_dict)} edges have been found between {args.dbsource} and {args.dbtarget}") + return path_dict + + +if __name__ == '__main__': + main() diff --git a/autobloody/proxy_bypass.py b/autobloody/proxy_bypass.py new file mode 100644 index 0000000..5a0c153 --- /dev/null +++ b/autobloody/proxy_bypass.py @@ -0,0 +1,92 @@ +from ctypes import * +import os +import socket +import platform +from bloodyAD import utils + +LOG = utils.LOG + +class ProxyBypass(): + proxy_connect = None + + def __init__(self): + proxy_detected = 'LD_PRELOAD' in os.environ and 'proxychains' in os.environ['LD_PRELOAD'] + LOG.info("[*] Connection to Neo4j") + if not proxy_detected: + LOG.info("[*] No proxy detected") + return + supported_platform = platform.system() in ["Darwin", "Linux"] + if not supported_platform: + LOG.warning(f"[-] Proxy detected but {plateform.system()} is not currently supported. Please raise an issue on the Github repo") + return + + self.proxy_connect = socket.socket.connect + + socket.socket.connect = real_connect + LOG.info("[+] Proxy bypass enabled for Neo4j connection") + + def disable(self): + if self.proxy_connect: + socket.socket.connect = self.proxy_connect + LOG.info("[+] Proxy bypass disabled") + + +class c_addrinfo(Structure): + pass +c_addrinfo._fields_ = [ + ('ai_flags', c_int), + ('ai_family', c_int), + ('ai_socktype', c_int), + ('ai_protocol', c_int), + ('ai_addrlen', c_size_t), + ] + ([ + ('ai_canonname', c_char_p), + ('ai_addr', POINTER(c_sockaddr_in)), + ] if platform.system() == 'Darwin' else [ + ('ai_addr', c_void_p), + ('ai_canonname', c_char_p), + ]) + [ + ('ai_next', POINTER(c_addrinfo)), +] + +def real_connect(sock_obj, addro): + libc = CDLL('libc.so.6') + get_errno_loc = libc.__errno_location + get_errno_loc.restype = POINTER(c_int) + def errcheck(ret, func, args): + if ret == -1: + e = get_errno_loc()[0] + raise OSError(e) + return ret + + # addr = c_sockaddr_in(sock_obj.family, c_ushort(socket.htons(addro[1])), (c_byte *4)(*[int(i) for i in addro[0].split('.')])) + # size_addr = sizeof(addr) + c_getaddrinfo = libc.getaddrinfo + c_getaddrinfo.errcheck = errcheck + presult = POINTER(c_addrinfo)() + hints = c_addrinfo() + hints.ai_family = sock_obj.family + hints.ai_socktype = sock_obj.type + hints.ai_flags = 0 + hints.ai_protocol = sock_obj.proto + c_getaddrinfo(addro[0].encode('utf-8'), str(addro[1]).encode('utf-8'), byref(hints), byref(presult)) + + # Wait until DB response + blocking = sock_obj.getblocking() + sock_obj.setblocking(True) + + c_connect = libc.connect + c_connect.errcheck = errcheck + c_connect(sock_obj.fileno(), c_void_p(presult.contents.ai_addr), presult.contents.ai_addrlen) + + libc.freeaddrinfo(presult) + + sock_obj.setblocking(blocking) + +# class c_sockaddr_in(Structure): +# _fields_ = [ +# ('sa_family', c_ushort), +# ('sin_port', c_ushort), +# ("sin_addr", c_byte * 4), +# ("__pad", c_byte * 8) +# ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1670e06..945c9b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -bloodyAD>=0.1 -neo4j>=4.4.6 \ No newline at end of file +. \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dabe6b4 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup +from pathlib import Path +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + +setup(name='autobloody', + version='0.1.0', + description='AD Privesc Automation', + long_description=long_description, + long_description_content_type='text/markdown', + author='CravateRouge', + author_email='baptiste.crepin@ntymail.com', + url='https://github.com/CravateRouge/autobloody', + download_url='https://github.com/CravateRouge/bloodyAD/archive/refs/tags/v0.1.0.tar.gz', + packages=['autobloody'], + license='MIT', + install_requires=['bloodyAD>=0.1','neo4j>=4.4.6'], + keywords = ['Active Directory', 'Privilege Escalation'], + classifiers=[ + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10' + ], + python_requires='>=3.6', + entry_points={ + "console_scripts":["autobloody = autobloody.main:main"] + } +) \ No newline at end of file