// HackTheBox  ·  writeup

Nubilum2

HackTheBox Easy PythonjqSplunk
Cloud

Scenario

Leading telecoms provider Forela uses AWS S3 as an essential part of their infrastructure. They can deploy applications quickly and do effective analytics on their sizable dataset thanks to it acting as both an application storage and a data lake storage.

Recently, a user reported an urgent issue to the helpdesk: an inability to access files within a designated S3 directory. This disruption has not only impeded critical operations but has also raised immediate security concerns.

The urgency of this situation demands a security-focused approach. Reports of a misconfigured S3 Bucket policy for the forela-fileshare bucket, resulting in unintended public access, highlight a potential security vulnerability that calls for immediate corrective measures. Consequently, a thorough investigation is paramount.

In order to rectify this situation, you are tasked to diligently identify who initiated the actions, how these actions were executed, and what specifically transpired with the S3 objects, leading to their inaccessibility. This comprehensive understanding is essential for a targeted and effective resolution.

Initiating with a comprehensive analysis of the AWS CloudTrail logs will provide a detailed record of all interactions with the affected S3 bucket. This step is pivotal in comprehending the entire extent of the incident and ensuring the necessary actions are taken to address it from a security perspective.

Executive Summary

An attacker found a publicly readable S3 bucket and downloaded files containing IAM access keys without authenticating. They then used the leaked keys from an aws-cli client, probing EC2 and IAM with dozens of denied calls until one user was able to write an inline policy to itself. That policy granted full administrative rights. With admin access the attacker re-encrypted bucket objects under a KMS key, dropped a ransom note into the bucket, and minted a fresh access key on a second IAM user for persistent access. The helpdesk ticket about inaccessible files was the visible result: the data was readable only with a key the victims could not use.

Artifacts

Timestamp note: CloudTrail times are UTC in ISO format, which matches the YYYY-MM-DDTHH:MM:SSZ answer format directly.

Initial Access

Q1 — What was the originating IP address the Threat Actor (TA) used to infiltrate the Forela’s AWS account?

Start with the forela-fileshare S3 events. Unauthenticated GetObject calls against the key files, followed by authenticated API use from the same address with an aws-cli user agent identifying a Kali client, point at one external IP. That address is the attacker.

jq -r '.[] | select(.eventSource=="s3.amazonaws.com") | [.eventTime,.sourceIPAddress,.userIdentity.type,.userAgent,.eventName] | @tsv' all.json
Click flag to reveal 54.242.59.197
Q2 — What was the time, filename, and Account ID of the first recorded s3 object accessed by the TA?

Filter the attacker IP from Q1 to GetObject and take the earliest hit. The file is a CSV of EC2 read-only keys, and the request carries no credentials at all, which is what makes the bucket policy the entry point.

jq -r '.[] | select(.sourceIPAddress=="54.242.59.197" and .eventName=="GetObject") | [.eventTime,.requestParameters.key,.userIdentity.accountId] | @tsv' all.json | sort | head -1

Gotcha: The account ID field is not the bucket owner’s account. The request was anonymous, and userIdentity.accountId literally holds anonymous, which is what the answer wants.

Click to reveal answer 2023-11-02T14:52:03Z,prod-EC2-readonly_accessKeys.csv,anonymous

Credential Access

Q3 — How many Access Keys were compromised, at a minimum?

Count distinct access key IDs that appear for the attacker IP and add the keys whose files were pulled from the bucket. The minimum is the number of keys the attacker demonstrably downloaded or used.

jq -r '.[] | select(.sourceIPAddress=="54.242.59.197") | .userIdentity.accessKeyId // empty' all.json | sort -u | wc -l

Gotcha: A first count of 6 was one short. Include every key the attacker downloaded or used, not just the ones that signed requests in a single view.

Click flag to reveal 7

Discovery

Q4 — The TA executed a command to filter EC2 instances. What were the name and value used for filtering?

DescribeInstances events from the attacker IP carry the filter in requestParameters.filterSet. At 15:05:58Z the filter selects running instances only.

jq -r '.[] | select(.eventName=="DescribeInstances") | [.eventTime,(.requestParameters.filterSet|tostring)] | @tsv' all.json
Click to reveal answer instance-state-name:running
Q5 — Can you provide the count of unsuccessful discovery and privilege escalation attempts made by the TA before gaining elevated access with the compromised keys?

Count attacker events with an AccessDenied error that occur before the first successful PutUserPolicy (Q7). The keyed calls account for 41 of them.

jq -r '.[] | select(.sourceIPAddress=="54.242.59.197" and (.errorCode // "" | test("AccessDenied|UnauthorizedOperation")) and .eventTime < "2023-11-02T15:21:32Z") | .eventName' all.json | wc -l

Gotcha: The count is 42, not 41. The extra one is the anonymous ListObjects against forela-storage that was denied, which also counts as a failed discovery attempt.

Click flag to reveal 42

Privilege Escalation

Q6 — Which IAM user successfully gained elevated privileges in this incident?

The pivot from Q5 is the first successful IAM write after the denials: a PutUserPolicy where the caller and the target user are the same account. That user granted itself the policy.

jq -r '.[] | select(.eventName=="PutUserPolicy") | [.eventTime,.userIdentity.userName,.requestParameters.userName,.requestParameters.policyName] | @tsv' all.json
Click to reveal answer dev-policy-specialist
Q7 — Which event name permitted the threat actor to generate an admin-level policy?

The event is the PutUserPolicy call from Q6, logged at 15:21:32Z. This one was reasoned rather than read off a dedicated artifact: the logs show the write succeeding right after the denied attempts, and the policy body in Q8 confirms it carries admin rights. The user could attach a policy to itself because it held iam:PutUserPolicy.

jq -r '.[] | select(.eventName=="PutUserPolicy") | [.eventTime,.eventName,.errorCode] | @tsv' all.json
Click flag to reveal PutUserPolicy
Q8 — What is the name and statement of the policy that was created that gave a standard user account elevated privileges?

requestParameters of the same PutUserPolicy event holds both the policy name and the JSON document. The statement allows every action on every resource, which is full administrator.

jq '.[] | select(.eventName=="PutUserPolicy") | .requestParameters' all.json

Gotcha: The answer format is name,[policyStatement] with the statement copied as it appears in the log’s requestParameters.

Click to reveal answer sbhyy79zky,[{"Effect": "Allow","Action": "*","Resource": "*"}]

Impact

Q9 — What was the ARN (Amazon Resource Name) used to encrypt the files?

With admin rights the attacker re-encrypted bucket objects with CopyObject with SSE-KMS. The KMS key ARN sits in the request parameters of those events, and it is a multi-Region key in us-east-1.

jq -r '.[] | select(.eventName=="CopyObject") | [.eventTime,.requestParameters["x-amz-server-side-encryption-aws-kms-key-id"]] | @tsv' all.json
Click flag to reveal arn:aws:kms:us-east-1:263954014653:key/mrk-85e24f85d964469cba9e4589335dd0f4
Q10 — What was the name of the file that the TA uploaded to the S3 bucket?

A PutObject at 15:26:22Z from the attacker IP, right after the re-encryption, writes the ransom note.

jq -r '.[] | select(.eventName=="PutObject" and .sourceIPAddress=="54.242.59.197") | [.eventTime,.requestParameters.key] | @tsv' all.json
Click to reveal answer README2DECRYPT.txt
Q12 — What action was the user not authorized to perform to view or download the file in the S3 bucket?

This is the helpdesk symptom. At 16:02:34Z forela-john called GetObject and got AccessDenied. The error message names the missing permission, kms:Decrypt, because the object is now encrypted under the attacker’s KMS key from Q9.

jq -r '.[] | select(.eventName=="GetObject" and .userIdentity.userName=="forela-john" and .errorCode=="AccessDenied") | [.eventTime,.errorMessage] | @tsv' all.json
Click to reveal answer kms:Decrypt

Persistence

Q11 — Which IAM user account did the TA modify in order to gain additional persistent access?

Seconds after dropping the note (15:26:53Z), a CreateAccessKey event creates a new long-lived key on another IAM user. A key that the attacker generated survives the cleanup of the leaked ones.

jq -r '.[] | select(.eventName=="CreateAccessKey") | [.eventTime,.userIdentity.userName,.requestParameters.userName] | @tsv' all.json
Click flag to reveal forela-admin

Attack Summary

PhaseAction
Initial AccessAnonymous GetObject of an access-key CSV from a publicly readable bucket
Credential AccessSeven leaked access keys downloaded or used from an aws-cli client
DiscoveryDescribeInstances filtered to running instances; dozens of denied discovery and IAM calls
Privilege Escalationdev-policy-specialist writes an allow-all inline policy to itself with PutUserPolicy
ImpactBucket objects rewritten with CopyObject under an SSE-KMS key
ImpactREADME2DECRYPT.txt uploaded to the bucket
PersistenceCreateAccessKey on forela-admin
Impactforela-john denied kms:Decrypt on the encrypted objects

Forensic Timeline

Timestamp (UTC)Event
2023-11-02 14:52:03First anonymous GetObject of prod-EC2-readonly_accessKeys.csv
15:05:58DescribeInstances filtered on instance-state-name:running
15:21:32PutUserPolicy grants the allow-all policy
15:26:22README2DECRYPT.txt uploaded with PutObject
15:26:53CreateAccessKey on forela-admin
16:02:34forela-john denied kms:Decrypt on GetObject

Times after the first row are taken from the same CloudTrail export; their date is not restated in the case notes.

IOCs

TypeValue
IP54.242.59[.]197
User agentaws-cli (Kali)
FileREADME2DECRYPT.txt
IAM policysbhyy79zky (inline, Action: *, Resource: *)
IAM user (escalated)dev-policy-specialist
IAM user (new key)forela-admin
KMS key ARNarn:aws:kms:us-east-1:263954014653:key/mrk-85e24f85d964469cba9e4589335dd0f4

MITRE ATT&CK

TechniqueIDDescription
Data from Cloud StorageT1530Anonymous download of access-key files from the public bucket
Valid Accounts: Cloud AccountsT1078.004Leaked IAM keys used from aws-cli
Cloud Infrastructure DiscoveryT1580DescribeInstances with a running-state filter
Account Manipulation: Additional Cloud RolesT1098.003Allow-all inline policy added with PutUserPolicy
Data Encrypted for ImpactT1486Objects re-encrypted under an attacker-chosen KMS key, ransom note dropped
Account Manipulation: Additional Cloud CredentialsT1098.001New access key created for forela-admin

Defender Takeaways

A public bucket held the keys to the account. Credential files should never sit in S3, and forela-fileshare should have had Block Public Access on. The whole intrusion began with an unauthenticated GetObject, which S3 data events log and which would have been a clear alert.

Dozens of AccessDenied events from one key is a signal, not noise. The attacker burned 41 keyed calls guessing at permissions before anything worked. An alert on a burst of AccessDenied or UnauthorizedOperation from a single principal or IP would have fired well before the escalation.

Users should not be able to write policy to themselves. iam:PutUserPolicy on its own principal turned a standard user into an administrator in one call. Deny self-targeted IAM writes, set permission boundaries, and alert on every PutUserPolicy, AttachUserPolicy and CreateAccessKey.

Ransomware in the cloud is a key-management event. Nothing was deleted: CopyObject with a KMS key the victims could not use was enough. Restrict who can use kms:Encrypt and kms:GenerateDataKey on new keys, enable object versioning so the original versions survive, and watch for unexpected KMS key IDs in S3 write requests.

Conclusion

An anonymous download from one misconfigured bucket gave the attacker the keys, a self-granted policy gave them admin, and a KMS key they controlled locked the data away, all within roughly an hour of the first request. CloudTrail recorded every step, and the missed signal was the wall of denied calls before the escalation.