Added getOutboundControlled.py

This commit is contained in:
mgeeky 2022-04-15 14:18:09 +02:00
parent f4e7309a3f
commit 096d0f9d9f
7 changed files with 302 additions and 35 deletions

@ -1 +1 @@
Subproject commit a1f5ba28880e6470fa3e8a59ba50388272db1839
Subproject commit 5830ad897e323325e854e70b7c69ffad623b7d17

@ -1 +1 @@
Subproject commit 6ce9975ae639ac16b7dce5c6461a066d8988cec8
Subproject commit 75f6270d0417d749b56c718d0d8ad0003c74d785

@ -1 +1 @@
Subproject commit 9bc13dacc3e21ab9774b059f73f8daadfccdebf6
Subproject commit 7d3c3b5991cb430d6ff9c66c794688d67d5baa75

View File

@ -4,6 +4,8 @@
- **`backdoor-drop.js`** - Internet Explorer - JavaScript trojan/backdoor dropper template, to be used during Penetration Testing assessments. ([gist](https://gist.github.com/mgeeky/b0aed7c1e510560db50f96604b150dac))
- **`bloodhound`** - bunch of BloodHound utilities & scripts
- **`Bypass-ConstrainedLanguageMode`** - Tries to bypass AppLocker Constrained Language Mode via custom COM object (as documented by @xpn in: https://www.mdsec.co.uk/2018/09/applocker-clm-bypass-via-com/ )
The way it does so is by registering a custom COM object (`InProcServer32` DLL) that will act as a native *.NET CLR4* host. This host is then going to load up a managed assembly within it's current AppDomain. That assembly finally will switch `SessionData.LanguageMode` variable determining whether Constrained Language Mode shall be used within current Runspace. More details in the tool directory itself.
@ -287,24 +289,6 @@ PS E:\PowerSploit\Recon> Get-DomainOU | Get-DomainOUTree
- **`Invoke-Command-Cred-Example.ps1`** - Example of using PSRemoting with credentials passed directly from command line. ([gist](https://gist.github.com/mgeeky/de4ecf952ddce774d241b85cfbf97faf))
- **`markOwnedNodesInNeo4j.py`** - This script takes an input file containing Node names to be marked in Neo4j database as owned = True. The strategy for working with neo4j and Bloodhound becomes fruitful during complex Active Directory Security Review assessments or Red Teams. Imagine you've kerberoasted a number of accounts, access set of workstations or even cracked userPassword hashes. Using this script you can quickly instruct Neo4j to mark that principals as owned, which will enrich your future use of BloodHound.
```bash
$ ./markOwnedNodesInNeo4j.py kerberoasted.txt
[.] Connected to neo4j instance.
[.] Marking nodes (0..10) ...
[+] Marked 10 nodes in 4.617 seconds. Finish ETA: in 16.622 seconds.
[.] Marking nodes (10..20) ...
[+] Marked 10 nodes in 4.663 seconds. Finish ETA: in 12.064 seconds.
[.] Marking nodes (20..30) ...
[+] Marked 10 nodes in 4.157 seconds. Finish ETA: in 7.167 seconds.
[.] Marking nodes (30..40) ...
[+] Marked 10 nodes in 4.365 seconds. Finish ETA: in 2.670 seconds.
[.] Marking nodes (40..46) ...
[+] Marked 6 nodes in 2.324 seconds. Finish ETA: in 0 seconds.
[+] Nodes marked as owned successfully in 20.246 seconds.
```
- **`msbuild-powershell-msgbox.xml`** - Example of Powershell execution via MSBuild inline task XML file. On a simple Message-Box script.
([gist](https://gist.github.com/mgeeky/617c54a23f0c4e99e6f475e6af070810))

View File

@ -0,0 +1,32 @@
## Bloodhound related utilities & scripts
- **`getOutboundControlled.py`** - Takes list of node names on input (must be in `NAME@DOMAIN` form) and for each node computes number of first-degree outbound controlled (or also number of group-delegated outbound controlled if specified so), then prints output CSV table containing these results. Handy to estimate number of outbound controlled objects through compromise of an input list of users.
```
$ py ./getOutboundControlled.py -o affected-users-outbound.csv affected-users.txt
[+] Connected to database. Working...
[+] Checked 5/1282 nodes in 7.381 seconds. Finish ETA: in 1885.190 seconds.
[+] Checked 10/1282 nodes in 5.259 seconds. Finish ETA: in 1607.888 seconds.
[+] Checked 15/1282 nodes in 7.204 seconds. Finish ETA: in 1676.210 seconds.
[+] Checked 20/1282 nodes in 7.152 seconds. Finish ETA: in 1703.490 seconds.
[+] Checked 25/1282 nodes in 6.109 seconds. Finish ETA: in 1664.574 seconds.
...
```
- **`markNodesOwned.py`** - This script takes an input file containing Node names to be marked in Neo4j database as owned = True. The strategy for working with neo4j and Bloodhound becomes fruitful during complex Active Directory Security Review assessments or Red Teams. Imagine you've kerberoasted a number of accounts, access set of workstations or even cracked userPassword hashes. Using this script you can quickly instruct Neo4j to mark that principals as owned, which will enrich your future use of BloodHound.
```bash
$ ./markNodesOwned.py kerberoasted.txt
[.] Connected to neo4j instance.
[.] Marking nodes (0..10) ...
[+] Marked 10 nodes in 4.617 seconds. Finish ETA: in 16.622 seconds.
[.] Marking nodes (10..20) ...
[+] Marked 10 nodes in 4.663 seconds. Finish ETA: in 12.064 seconds.
[.] Marking nodes (20..30) ...
[+] Marked 10 nodes in 4.157 seconds. Finish ETA: in 7.167 seconds.
[.] Marking nodes (30..40) ...
[+] Marked 10 nodes in 4.365 seconds. Finish ETA: in 2.670 seconds.
[.] Marking nodes (40..46) ...
[+] Marked 6 nodes in 2.324 seconds. Finish ETA: in 0 seconds.
[+] Nodes marked as owned successfully in 20.246 seconds.
```

View File

@ -0,0 +1,228 @@
#!/usr/bin/python3
#
# getOutboundControlled.py
#
# Collects first-degree and group-delegated outbound controlled objects number based on input node names list.
#
# Mariusz Banach / mgeeky
#
import sys
import os
import time
import math
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
try:
from neo4j import GraphDatabase
except ImportError:
print('[!] "neo4j >= 1.7.0" required. Install it with: python3 -m pip install neo4j')
#
# ===========================================
#
config = {
'host': 'bolt://localhost:7687',
'user': 'neo4j',
'pass': 'neo4j1',
'output' : '',
'include_group_delegated' : False
}
#
# ===========================================
#
nodesToCheckPerStep = 5
columns1 = 'name,outbound_first_degree'
columns2 = 'name,outbound_first_degree,outbount_group_delegated'
query_first_degree_outbound = '''
MATCH p=(u)-[r1]->(n) WHERE r1.isacl=true AND (CONDITION)
WITH u.name as name, COUNT(DISTINCT(n)) as controlled
RETURN name, controlled
'''
query_group_delegated_outbound = '''
MATCH p=(u)-[r1:MemberOf*1..]->(g:Group)-[r2]->(n) WHERE r2.isacl=true AND (CONDITION)
WITH u.name as name, COUNT(DISTINCT(n)) as controlled
RETURN name, controlled
'''
results = {}
def checkNodes(tx, nodes):
global results
conditionList = []
for node in nodes:
conditionList.append(f'u.name = "{node}"')
if node not in results.keys():
results[node] = {
'name' : node,
'first-degree' : 0,
'group-delegated': 0,
}
condition = ' OR '.join(conditionList)
interimResults = {}
for node in nodes:
interimResults[node] = {
'name' : node,
'first-degree' : 0,
'group-delegated' : 0,
}
# first-degree
query = query_first_degree_outbound.replace('CONDITION', condition).strip().replace('\t', ' ').replace('\n', ' ')
result1 = list(tx.run(query))
for result in result1:
interimResults[result['name']]['first-degree'] = result['controlled']
if config['include_group_delegated']:
# group delegated
query = query_group_delegated_outbound.replace('CONDITION', condition).strip().replace('\t', ' ').replace('\n', ' ')
result2 = list(tx.run(query))
for result in result2:
interimResults[result['name']]['group-delegated'] = result['controlled']
results.update(interimResults)
if len(config['output']) > 0:
output = ''
for k, v in interimResults.items():
if config['include_group_delegated']:
output += f"{v['name']},{v['first-degree']},{v['group-delegated']}\n"
else:
output += f"{v['name']},{v['first-degree']}\n"
with open(config['output'], 'a') as f:
f.write(output)
def log(x):
sys.stderr.write(x + '\n')
def opts(args):
global config
parser = ArgumentParser(description = 'getOutboundControlled.py - collects first-degree and group-delegated outbound controlled objects number based on input node names list.', formatter_class = ArgumentDefaultsHelpFormatter)
parser.add_argument('-H', '--host', dest = 'host', help = 'Neo4j BOLT URI', default = 'bolt://localhost:7687')
parser.add_argument('-u', '--user', dest = 'user', help = 'Neo4j User', default = 'neo4j')
parser.add_argument('-p', '--password', dest = 'pass', help = 'Neo4j Password', default = 'neo4j1')
parser.add_argument('-g', '--include-group-delegated', dest = 'include_group_delegated', action='store_true', help = 'To optimize time the script by default only computes number of first-degree outbound controlled objects. Use this option to include in final results also group-delegated numbers (takes considerable time to evaluate!)')
parser.add_argument('-o', '--output', dest = 'output', help = 'Write output to CSV file specified by this path.', default = '<stdout>')
parser.add_argument('nodesList', help = 'Path to file containing list of node names to check. Lines starting with "#" will be skipped.')
arguments = parser.parse_args()
config.update(vars(arguments))
return arguments
def main(argv):
if len(argv) < 2:
print('''
Takes a file containing node names on input and computes number of Outbound controlled objects by those nodes.
Usage: ./getOutboundControlled.py [options] <nodes-file>
''')
return False
args = opts(argv)
nodesFile = args.nodesList
programStart = time.time()
if not os.path.isfile(nodesFile):
log(f'[!] Input file containing nodes does not exist: "{nodesFile}"!')
return False
nodes = []
with open(nodesFile) as f:
for x in f.readlines():
if x.strip().startswith('#'):
continue
if not '@' in x:
raise Exception('Node names must include "@" and be in form: NAME@DOMAIN !')
nodes.append(x.strip())
try:
driver = GraphDatabase.driver(
config['host'],
auth = (config['user'], config['pass']),
encrypted = False,
connection_timeout = 10,
keep_alive = True
)
except Exception as e:
log(f'[-] Could not connect to the neo4j database. Reason: {str(e)}')
return False
finishEta = 0.0
totalTime = 0.0
runs = 0
try:
with driver.session() as session:
log('[+] Connected to database. Working...')
output = ''
if config['include_group_delegated']:
output = columns2 + '\n'
else:
output = columns1 + '\n'
with open(config['output'], 'w') as f:
f.write(output)
for a in range(0, len(nodes), nodesToCheckPerStep):
b = a + min(nodesToCheckPerStep, len(nodes) - a)
start = time.time()
checkNodes(session, nodes[a:b])
stop = time.time()
totalTime += (stop - start)
runs += 1
finishEta = ((len(nodes) / nodesToCheckPerStep) - runs) * (totalTime / float(runs))
if finishEta < 0:
finishEta = 0
log(f'[+] Checked {b}/{len(nodes)} nodes in {stop - start:.3f} seconds. Finish ETA: in {finishEta:.3f} seconds.')
except KeyboardInterrupt:
log('[.] User interrupted.')
driver.close()
return False
driver.close()
programStop = time.time()
log(f'\n[+] Nodes checked in {programStop - programStart:.3f} seconds.')
if config['output'] == '':
for k, v in results.items():
if config['include_group_delegated']:
output += f"{v['name']},{v['first-degree']},{v['group-delegated']}\n"
else:
output += f"{v['name']},{v['first-degree']}\n"
print(output)
else:
log(f'[+] Finished. Results written to file:\n\t{config["output"]}')
if __name__ == '__main__':
main(sys.argv)

View File

@ -13,6 +13,9 @@
import sys
import os
import time
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
try:
from neo4j import GraphDatabase
except ImportError:
@ -22,15 +25,13 @@ except ImportError:
# ===========================================
#
# Specify neo4j connection details
NEO4J_CONNECTION_DETAILS = \
{
'Host': '127.0.0.1', # neo4j listening address.
'Port': 7687, # Bolt port
'User': 'neo4j',
'Pass': 'neo4j1'
config = {
'host': 'bolt://localhost:7687',
'user': 'neo4j',
'pass': 'neo4j1',
}
#
# ===========================================
#
@ -53,32 +54,54 @@ def markNodes(tx, nodes):
tx.run(query)
def opts(args):
global config
parser = ArgumentParser(description = 'markNodesOwned.py - collects first-degree and group-delegated outbound controlled objects number based on input node names list.', formatter_class = ArgumentDefaultsHelpFormatter)
parser.add_argument('-H', '--host', dest = 'host', help = 'Neo4j BOLT URI', default = 'bolt://localhost:7687')
parser.add_argument('-u', '--user', dest = 'user', help = 'Neo4j User', default = 'neo4j')
parser.add_argument('-p', '--password', dest = 'pass', help = 'Neo4j Password', default = 'neo4j1')
parser.add_argument('nodesList', help = 'Path to file containing list of node names to check. Lines starting with "#" will be skipped.')
arguments = parser.parse_args()
config.update(vars(arguments))
return arguments
def main(argv):
if len(argv) != 2:
print('''
Takes a file containing node names on input and marks them as Owned in specified neo4j database.
Usage: ./markOwnedNodesInNeo4j.py <nodes-file>
Usage: ./markNodesOwned.py <nodes-file>
''')
return False
nodesFile = argv[1]
nodesFile = args.nodesList
programStart = time.time()
if not os.path.isfile(nodesFile):
print(f'[!] Input file containing nodes does not exist: "{nodesFile}"!')
log(f'[!] Input file containing nodes does not exist: "{nodesFile}"!')
return False
nodes = []
with open(nodesFile) as f: nodes = [x.strip() for x in f.readlines()]
with open(nodesFile) as f:
for x in f.readlines():
if x.strip().startswith('#'):
continue
if not '@' in x:
raise Exception('Node names must include "@" and be in form: NAME@DOMAIN !')
nodes.append(x.strip())
try:
driver = GraphDatabase.driver(
f"bolt://{NEO4J_CONNECTION_DETAILS['Host']}:{NEO4J_CONNECTION_DETAILS['Port']}",
auth = (NEO4J_CONNECTION_DETAILS['User'], NEO4J_CONNECTION_DETAILS['Pass']),
config['host'],
auth = (config['user'], config['pass']),
encrypted = False,
connection_timeout = 10,
max_retry_time = 5,
keep_alive = True
)
except Exception as e: