Your digital forensics expertise is critical to determine whether data exfiltration has occurred from the customer’s environment. Initial findings include a compromised AWS credential, indicating a potential unauthorized access. This investigation follows from a customer report of leaked data allegedly for sale on the darknet market. By examining the compromised server and analyzing the provided AWS logs, it will not only validate the customer’s breach report but also provide valuable insights into the attacker’s tactics, enabling a more comprehensive response.
An external host fuzzed the victim’s web server with a wordlist until it found the application path, then abused a flaw in a PHP page to make the server fetch resources on the attacker’s behalf. The first fetches read local files, and the next reached the cloud instance metadata service. That yielded credentials for the instance role, while a separate leaked IAM user key was used from the same infrastructure. The actor ran identity and infrastructure discovery calls across several regions, read database contents from the server’s shell history, and created snapshots of two production RDS databases. The snapshots were then shared with an AWS account the actor controlled, which completed the exfiltration entirely inside the AWS API.
uac tarball from the web server → extract with tar. Holds the Apache access.log and error.log, shell history, .mysql_history and the web rootFormat note: Q8 and Q11 want comma-separated values in ascending order, and Q8 only wants regions where the IAM user’s credentials succeeded.
Extract the collection and open the Apache access.log. One source, 35.169.66.138, produces a burst of requests for random and dictionary-style paths that nearly all return 404, which is directory and file brute forcing from a wordlist. MITRE files that as an Active Scanning sub-technique (T1595.003), and the name of that sub-technique is the answer.
tar xzf uac -C work/tar
grep -c ' 404 ' access.log; grep 35.169.66.138 access.log | head
Filter the log to everything that is not a 404 and look for redirects. The request for /index.php comes back with a 3xx, and a redirect is what discloses a real path to a scanner that was otherwise getting only misses.
grep -vE ' 404 ' access.log | grep -E ' 30[0-9] '
The Apache error.log records the application’s Input: lines for the user-supplied value. The first one using a file:// scheme names the local file the actor tried to read before moving on to the metadata address. The format hint (****:///***/******) matches a file:/// URI.
grep 'Input:' error.log | head
The page passes user input straight into file_get_contents, so the server itself fetches whatever URL or path it is given. That lets an outside attacker reach 169.254.169.254, which is only routable from the instance. The grep for that address in the logs confirms the target.
grep -r '169.254.169.254' error.log access.log
The first Input: entry in error.log pointing at http://169.254.169.254 gives the time. It is the first request to the metadata service, ahead of any credential path under it.
grep 'Input: http://169.254.169.254' error.log | head -1
The server’s database client history lists what the actor, or the admin before them, queried. .mysql_history names the customer table directly. A raw string search across the extracted collection for table names leads to the same file.
cat .mysql_history
Tip: The format hint ends in
o, which narrows the table name down before you open the file.
Reference knowledge: the STS call that returns the account, user ID and ARN of the calling credentials is the standard first move with stolen keys. It is GetCallerIdentity, and it needs no permissions, so it works with any valid key. Filtering the merged CloudTrail JSONL on that eventName shows the actor running it.
python3 -I summarize_ct.py work/ct/all.jsonl --by eventName
The CloudTrail files are thousands of small gzips, so merge them into one JSONL first. Then filter to the leaked IAM user (devops-ash), the attacker source IPs 35.169.66.138 and 34.202.84.37, and events with no errorCode. Group by awsRegion; two regions have successful calls.
python3 -I merge_ct.py work/tar work/ct/all.jsonl
python3 -I summarize_ct.py work/ct/all.jsonl --user devops-ash --success --by awsRegion
Gotcha: Counting every region seen from the actor’s IPs gives 17, but that includes calls made with the EC2 instance role from Q4. The question is about the IAM user’s credentials only, and that user succeeded in just two regions.
EC2 lists instances, including their public IPs, through a single describe call. It shows up in the IAM user’s early activity in CloudTrail, ahead of the web server exploitation.
python3 -I summarize_ct.py work/ct/all.jsonl --user devops-ash --by eventName
Take every CloudTrail event from the two attacker addresses (35.169.66.138 and 34.202.84.37) that carries an errorCode, and count them. Both addresses have to be included.
python3 -I summarize_ct.py work/ct/all.jsonl --ips 35.169.66.138,34.202.84.37 --errors --count
Take successful events from the attacker IPs, drop the identity calls such as GetCallerIdentity, and list the distinct userIdentity.arn values. Two identities remain: the leaked IAM user and the assumed-role session of the web server instance, which is the metadata credential from Q5.
python3 -I summarize_ct.py work/ct/all.jsonl --ips 35.169.66.138,34.202.84.37 --success --exclude-identity --by arn
Filter CloudTrail to CreateDBSnapshot and read requestParameters.dBSnapshotIdentifier. Two snapshots were created, one per production database, with the creation time embedded in the name.
python3 -I summarize_ct.py work/ct/all.jsonl --event CreateDBSnapshot --field requestParameters.dBSnapshotIdentifier
Look at ModifyDBSnapshotAttribute events. The valuesToAdd field in the request parameters lists the account the snapshot was shared with, which is the destination account and differs from the victim account 949622803460.
python3 -I summarize_ct.py work/ct/all.jsonl --event ModifyDBSnapshotAttribute --field requestParameters.valuesToAdd
Sharing the snapshots with an account the actor owns is Transfer Data to Cloud Account, from MITRE ATT&CK’s Exfiltration tactic. No command is needed.
| Phase | Action |
|---|---|
| Reconnaissance | Wordlist scan of the web server from one external IP; a 301 on /index.php exposes the real path |
| Initial Access | Request-forgery payloads against the PHP page, starting with a local file read |
| Credential Access | Page used to reach the instance metadata service and obtain the EC2 role credentials |
| Discovery | Leaked IAM user and the role run identity and instance-listing calls, successfully in two regions for the user |
| Discovery | Database content reviewed through the server’s MySQL history |
| Collection | Snapshots created of two production RDS databases |
| Exfiltration | Snapshots shared with an attacker-owned AWS account |
| Type | Value |
|---|---|
| IP (scanner, SSRF, API calls) | 35.169.66[.]138 |
| IP (API calls) | 34.202.84[.]37 |
| URL (metadata service target) | hxxp[://]169[.]254[.]169[.]254 |
| IAM user (leaked) | arn:aws:iam::949622803460:user/devops-ash |
| Assumed role (instance) | arn:aws:sts::949622803460:assumed-role/EC2DatabaseConnection/i-0bdf91168b50e943e |
| RDS snapshot | transactiondb-prod-2024-03-13-06-53 |
| RDS snapshot | wb-customerdb-prod-2024-03-13-07-59 |
| AWS account (attacker) | 143637014344 |
| Technique | ID | Description |
|---|---|---|
| Active Scanning: Wordlist Scanning | T1595.003 | Dictionary path brute force against the web server |
| Exploit Public-Facing Application | T1190 | Request forgery against the PHP page |
| Unsecured Credentials: Cloud Instance Metadata API | T1552.005 | Role credentials pulled from the metadata service |
| System Owner/User Discovery | T1033 | GetCallerIdentity with the stolen keys |
| Cloud Infrastructure Discovery | T1580 | DescribeInstances and other infrastructure enumeration |
| Data from Information Repositories | T1213 | Database content reviewed through MySQL history |
| Modify Cloud Compute Infrastructure: Create Snapshot | T1578.001 | CreateDBSnapshot on two production RDS databases |
| Transfer Data to Cloud Account | T1537 | Snapshots shared with an attacker-owned account |
Block user-controlled URL fetches in the web app. The page handed input to file_get_contents, which allowed both local file reads and requests to link-local addresses. Allow-list the schemes and hosts the code may fetch, and drop file:// and metadata addresses outright.
Require IMDSv2 on every instance. A single forged GET returned usable role credentials. IMDSv2 needs a session token obtained with a PUT request and a header, which a simple fetch primitive cannot produce, and a hop limit of 1 keeps proxied requests off the metadata service.
Scan for and revoke leaked IAM keys fast. The devops-ash user key worked from external addresses, and the actor used it in more than one region. Short-lived credentials, source-IP conditions on the key and alerts on GetCallerIdentity from unfamiliar addresses would have shortened the window.
Alert on RDS snapshot sharing. ModifyDBSnapshotAttribute with a foreign account in valuesToAdd is a precise exfiltration signal, and CreateDBSnapshot outside a backup window is a good precursor. A service control policy can deny sharing to accounts outside the organization.
Watch the error volume. The actor generated hundreds of failed API calls while probing permissions. A spike of AccessDenied for one principal across many services is a cheap, early alert.
The actor turned one forgeable web request into AWS credentials, then moved two production databases into their own account without touching the servers again. Every step from the metadata call to the snapshot share is in CloudTrail, but nothing flagged a role session or a foreign account being granted access.