diff --git a/clouds/azure/AzureRT b/clouds/azure/AzureRT index a1f5ba2..5830ad8 160000 --- a/clouds/azure/AzureRT +++ b/clouds/azure/AzureRT @@ -1 +1 @@ -Subproject commit a1f5ba28880e6470fa3e8a59ba50388272db1839 +Subproject commit 5830ad897e323325e854e70b7c69ffad623b7d17 diff --git a/file-formats/PackMyPayload b/file-formats/PackMyPayload index 6ce9975..75f6270 160000 --- a/file-formats/PackMyPayload +++ b/file-formats/PackMyPayload @@ -1 +1 @@ -Subproject commit 6ce9975ae639ac16b7dce5c6461a066d8988cec8 +Subproject commit 75f6270d0417d749b56c718d0d8ad0003c74d785 diff --git a/phishing/decode-spam-headers b/phishing/decode-spam-headers index 9bc13da..7d3c3b5 160000 --- a/phishing/decode-spam-headers +++ b/phishing/decode-spam-headers @@ -1 +1 @@ -Subproject commit 9bc13dacc3e21ab9774b059f73f8daadfccdebf6 +Subproject commit 7d3c3b5991cb430d6ff9c66c794688d67d5baa75 diff --git a/red-teaming/README.md b/red-teaming/README.md index 961b5a0..df766a2 100755 --- a/red-teaming/README.md +++ b/red-teaming/README.md @@ -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)) diff --git a/red-teaming/bloodhound/README.md b/red-teaming/bloodhound/README.md new file mode 100644 index 0000000..ec23388 --- /dev/null +++ b/red-teaming/bloodhound/README.md @@ -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. +``` diff --git a/red-teaming/bloodhound/getOutboundControlled.py b/red-teaming/bloodhound/getOutboundControlled.py new file mode 100644 index 0000000..f154950 --- /dev/null +++ b/red-teaming/bloodhound/getOutboundControlled.py @@ -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 = '') + + 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] +''') + 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) diff --git a/red-teaming/markOwnedNodesInNeo4j.py b/red-teaming/bloodhound/markNodesOwned.py similarity index 69% rename from red-teaming/markOwnedNodesInNeo4j.py rename to red-teaming/bloodhound/markNodesOwned.py index cdeddd0..a501b5e 100644 --- a/red-teaming/markOwnedNodesInNeo4j.py +++ b/red-teaming/bloodhound/markNodesOwned.py @@ -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 +Usage: ./markNodesOwned.py ''') 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: