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 = []