From ae9c252524f8470535d26d7b08666a3a03844f91 Mon Sep 17 00:00:00 2001 From: aas <lylefebvre.infosec@gmail.com> Date: Sat, 10 Apr 2021 16:47:42 +0200 Subject: [PATCH 1/4] Neo4j >= 4 compatibility (shortestpath algorithm only) --- aclpwn/__init__.py | 2 +- aclpwn/database.py | 2 +- aclpwn/pathfinding.py | 17 +++++++++-------- aclpwn/utils.py | 4 ++-- requirements.txt | 1 + 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/aclpwn/__init__.py b/aclpwn/__init__.py index 14419f1..3a2eb98 100644 --- a/aclpwn/__init__.py +++ b/aclpwn/__init__.py @@ -103,7 +103,7 @@ def main(): paths = pathfinding.dijkstra_find_cypher(from_object, to_object, args.from_type.capitalize(), args.to_type.capitalize()) else: # First we need to obtain the node IDs for use with the REST api - q = "MATCH (n:%s {name: {name}}) RETURN n" + q = "MATCH (n:%s {name: $name}) RETURN n" with database.driver.session() as session: fromres = session.run(q % args.from_type.capitalize(), name=from_object) try: diff --git a/aclpwn/database.py b/aclpwn/database.py index 9d5d8d2..9dae1a2 100644 --- a/aclpwn/database.py +++ b/aclpwn/database.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals, print_function -from neo4j.v1 import GraphDatabase +from neo4j import GraphDatabase import platform import requests import json diff --git a/aclpwn/pathfinding.py b/aclpwn/pathfinding.py index 5077b5d..66d46e5 100644 --- a/aclpwn/pathfinding.py +++ b/aclpwn/pathfinding.py @@ -39,7 +39,7 @@ def dijkstra_find(fromid, toid, dbhost): return paths def dijkstra_find_cypher(startnode, endnode, starttype='User', endtype='User'): - query = "MATCH (n:%s {name: {startnode}}), (m:%s {name: {endnode}}) " \ + query = "MATCH (n:%s {name: $startnode}), (m:%s {name: $endnode}) " \ "CALL algo.shortestPath.stream(n, m, 'aclpwncost', " \ "{nodeQuery:null, relationshipQuery:null, defaultValue:200.0, direction:'OUTGOING'}) " \ "YIELD nodeId, cost " \ @@ -60,15 +60,15 @@ def dijkstra_find_cypher(startnode, endnode, starttype='User', endtype='User'): queries = { # Query all shortest paths - 'shortestonly': "MATCH (n:%s {name: {startnode}})," - "(m:%s {name: {endnode}})," + 'shortestonly': "MATCH (n:%s {name: $startnode})," + "(m:%s {name: $endnode})," " p=allShortestPaths((n)-[:MemberOf|AddMember|GenericAll|" "GenericWrite|WriteOwner|WriteDacl|Owns|DCSync|GetChangesAll|AllExtendedRights*1..]->(m))" " RETURN p", # Query all simple paths (more expensive query than above) # credits to https://stackoverflow.com/a/40062243 - 'allsimple': "MATCH (n:%s {name: {startnode}})," - "(m:%s {name: {endnode}})," + 'allsimple': "MATCH (n:%s {name: $startnode})," + "(m:%s {name: $endnode})," " p=(n)-[:MemberOf|AddMember|GenericAll|" "GenericWrite|WriteOwner|WriteDacl|Owns|DCSync|GetChangesAll|AllExtendedRights*1..]->(m)" "WHERE ALL(x IN NODES(p) WHERE SINGLE(y IN NODES(p) WHERE y = x))" @@ -77,11 +77,12 @@ def dijkstra_find_cypher(startnode, endnode, starttype='User', endtype='User'): def get_path(startnode, endnode, starttype='User', endtype='User', querytype='allsimple'): + records = [] with database.driver.session() as session: with session.begin_transaction() as tx: - return tx.run(queries[querytype] % (starttype, endtype), - startnode=startnode, - endnode=endnode) + for record in tx.run(queries[querytype] % (starttype, endtype), startnode=startnode, endnode=endnode): + records.append(record) + return records def get_path_cost(record): nmap = utils.getnodemap(record['p'].nodes) diff --git a/aclpwn/utils.py b/aclpwn/utils.py index 824f819..8790f9e 100644 --- a/aclpwn/utils.py +++ b/aclpwn/utils.py @@ -11,7 +11,7 @@ def print_path(record): # Iterate the path pathtext = '(%s)-' % record['p'].nodes[0].get('name') for el in record['p']: - pathtext += '[%s]->(%s)-' % (el.type, nmap[el.end].get('name')) + pathtext += '[%s]->(%s)-' % (el.type, nmap[el.end_node.id].get('name')) pathtext = pathtext[:-1] return pathtext @@ -20,7 +20,7 @@ def build_path(record): nmap = getnodemap(record['p'].nodes) # Iterate the path for el in record['p']: - path.append((el, nmap[el.end])) + path.append((el, nmap[el.end_node.id])) return path def build_rest_path(nodes, rels): diff --git a/requirements.txt b/requirements.txt index e6b1128..ca45b22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ neo4j-driver impacket ldap3 +requests From a5f2e235cb82f47e8ab991a73a0e1933abd8cfc3 Mon Sep 17 00:00:00 2001 From: aas <lylefebvre.infosec@gmail.com> Date: Sun, 11 Apr 2021 21:43:52 +0200 Subject: [PATCH 2/4] Neo4j >= 4 compatibility (dijkstra-cypher) --- aclpwn/pathfinding.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/aclpwn/pathfinding.py b/aclpwn/pathfinding.py index 66d46e5..d497bc6 100644 --- a/aclpwn/pathfinding.py +++ b/aclpwn/pathfinding.py @@ -40,21 +40,20 @@ def dijkstra_find(fromid, toid, dbhost): def dijkstra_find_cypher(startnode, endnode, starttype='User', endtype='User'): query = "MATCH (n:%s {name: $startnode}), (m:%s {name: $endnode}) " \ - "CALL algo.shortestPath.stream(n, m, 'aclpwncost', " \ - "{nodeQuery:null, relationshipQuery:null, defaultValue:200.0, direction:'OUTGOING'}) " \ - "YIELD nodeId, cost " \ - "RETURN nodeId as node, cost" + "CALL gds.beta.shortestPath.dijkstra.stream({sourceNode: id(n), targetNode: id(m), " \ + "relationshipWeightProperty: 'aclpwncost', nodeProjection: '*', relationshipProjection: {" \ + "all: {type: '*', properties: 'aclpwncost', orientation: 'NATURAL'}}}) " \ + "YIELD nodeIds, costs " \ + "RETURN nodeIds, costs" + path = [] with database.driver.session() as session: with session.begin_transaction() as tx: - print(query % (starttype, endtype)) - path = tx.run(query % (starttype, endtype), - startnode=startnode, - endnode=endnode, - property='aclpwncost') + for record in tx.run(query % (starttype, endtype), startnode=startnode, endnode=endnode, property='aclpwncost'): + path.append(record) paths = [] - nodes, rels = resolve_dijkstra_path(path) - paths.append((nodes, rels, path)) + nodes, rels = resolve_dijkstra_path(path[0]) + paths.append((nodes, rels, path[0])) return paths @@ -94,17 +93,17 @@ def get_path_cost(record): def resolve_dijkstra_path(path): nodes = [] rels = [] - nq = "MATCH (n)-[w {aclpwncost: {cost}}]->(m) WHERE ID(n) = {source} AND ID(m) = {dest} RETURN n,w,m" - bnq = "MATCH (n)-[w]->(m) WHERE ID(n) = {source} AND ID(m) = {dest} RETURN n,w,m" + nq = "MATCH (n)-[w {aclpwncost: $cost}]->(m) WHERE ID(n) = $source AND ID(m) = $dest RETURN n,w,m" + bnq = "MATCH (n)-[w]->(m) WHERE ID(n) = $source AND ID(m) = $dest RETURN n,w,m" with database.driver.session() as session: with session.begin_transaction() as tx: pv = path.values() - for i in range(1, len(pv)): - res = tx.run(nq, source=pv[i-1][0], cost=pv[i][1]-pv[i-1][1], dest=pv[i][0]) + for i in range(1, len(pv[0])): + res = tx.run(nq, source=pv[0][i-1], cost=pv[1][i]-pv[1][i-1], dest=pv[0][i]) data = res.single() # No result, most likely an invalid path, but query for a relationship with any cost regardless if not data: - res = tx.run(bnq, source=pv[i-1][0], dest=pv[i][0]) + res = tx.run(bnq, source=pv[0][i-1], dest=pv[0][i]) data = res.single() nodes.append(data['n']) rels.append(data['w']) From ccbfe50916148d216e17aea53d150edb5db8a32e Mon Sep 17 00:00:00 2001 From: aas <lylefebvre.infosec@gmail.com> Date: Tue, 18 May 2021 22:10:36 +0200 Subject: [PATCH 3/4] fix dijkstra-cypher --- aclpwn/pathfinding.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aclpwn/pathfinding.py b/aclpwn/pathfinding.py index d497bc6..6ba34ca 100644 --- a/aclpwn/pathfinding.py +++ b/aclpwn/pathfinding.py @@ -30,6 +30,7 @@ def dijkstra_find(fromid, toid, dbhost): "cost_property": "aclpwncost", "default_cost": 1 } + print(fromid) resp = database.restapi.post('http://%s:7474/db/data/node/%s/paths' % (dbhost, fromid), json=data) data = resp.json() paths = [] @@ -52,8 +53,9 @@ def dijkstra_find_cypher(startnode, endnode, starttype='User', endtype='User'): for record in tx.run(query % (starttype, endtype), startnode=startnode, endnode=endnode, property='aclpwncost'): path.append(record) paths = [] - nodes, rels = resolve_dijkstra_path(path[0]) - paths.append((nodes, rels, path[0])) + if path: + nodes, rels = resolve_dijkstra_path(path[0]) + paths.append((nodes, rels, path[0])) return paths From b447f20b2b2ea0905e6430f6e6bb85237f13e23f Mon Sep 17 00:00:00 2001 From: aas <lylefebvre.infosec@gmail.com> Date: Tue, 18 May 2021 22:27:30 +0200 Subject: [PATCH 4/4] remove a print :) --- aclpwn/pathfinding.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aclpwn/pathfinding.py b/aclpwn/pathfinding.py index 6ba34ca..920a0f5 100644 --- a/aclpwn/pathfinding.py +++ b/aclpwn/pathfinding.py @@ -30,7 +30,6 @@ def dijkstra_find(fromid, toid, dbhost): "cost_property": "aclpwncost", "default_cost": 1 } - print(fromid) resp = database.restapi.post('http://%s:7474/db/data/node/%s/paths' % (dbhost, fromid), json=data) data = resp.json() paths = []