Skip to content

Commit

Permalink
Add proxy bypass and merge pathgen with autobloody
Browse files Browse the repository at this point in the history
  • Loading branch information
CravateRouge committed Sep 10, 2022
1 parent cf186d5 commit 4009a49
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 45 deletions.
42 changes: 25 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
# ![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.

> :warning: **-ds and -dt values are case sensitive**
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
Expand All @@ -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
Expand All @@ -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
Expand Down
28 changes: 2 additions & 26 deletions autobloody.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions autobloody/database.py
Original file line number Diff line number Diff line change
@@ -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()

Expand Down
56 changes: 56 additions & 0 deletions autobloody/main.py
Original file line number Diff line number Diff line change
@@ -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()
92 changes: 92 additions & 0 deletions autobloody/proxy_bypass.py
Original file line number Diff line number Diff line change
@@ -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)
# ]
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
bloodyAD>=0.1
neo4j>=4.4.6
.
32 changes: 32 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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"]
}
)

0 comments on commit 4009a49

Please sign in to comment.