The cloud administration team is alerted to potential malicious activity occurring within their Amazon EC2 instances. Unrecognised deployments were discovered by the system administrator, posing a serious threat to the company’s reputation. The lack of preemptive security measures exacerbates the situation, including the unrestricted global access to the critical S3 bucket. The incident responder emphasises the urgency of investigating the misuse of their infrastructure, urging the necessity for immediate collaboration to collect CloudTrail data and initiate a Phase 1 UNIX collection on the implicated EC2 instance. The situation outlines a critical security lapse demanding swift action to protect the company’s reputation.
A globally readable S3 bucket exposed a backup script with a hard-coded IAM access key pair. The threat actor used those keys from a small block of addresses to call the EC2 API directly, generating key pairs, creating a permissive security group and launching batches of instances. The administrator terminated the first batch, but the actor kept deploying. A CatScale UNIX collection from one of the instances showed a PoshC2 server running, and its project database, locked inside a password-protected archive, listed the machines that had checked in.
userName, eventName and sourceIPAddressforela-storage bucket → extract and grep for key materialtar.gz from the EC2 host, with an unusual-directory.zip inside → extract, then read the process listing and the archiveTimestamp note: CloudTrail
eventTimeis UTC. Q5 wants the full ISO form with a trailingZ, Q8 wantsYYYY-MM-DD hh:mm.
Collection note: The CatScale tarball contains directories with mode
000. Runchmod -R u+rwXon the extracted tree before browsing it.
Extract the S3 bucket archives and search them for AWS access key IDs (AKIA...). One file, a backup script, carries an access_key and secret_key pair in plain text. The path inside the bucket, minus the bucket name, is the location.
grep -rn 'AKIA' work/s3
From Q2 the leaked key ID is AKIA52GPOBQCO4SRMOWK. The CloudTrail export is a folder of plain .json files, so merge them into one time-sorted list first, then read the userIdentity.userName on events that used that access key.
python3 -I merge_ct.py ct/ > work/ct/all.json
grep -o '"userName": *"[^"]*"' work/ct/all.json | sort | uniq -c | sort -rn
Filter the merged CloudTrail data to the compromised user from Q1 and list the distinct sourceIPAddress values. The administrator’s own activity comes from different addresses, so the leaked-key traffic separates cleanly. Four addresses in one /24 remain; sort them ascending as the format asks.
python3 -I ct_ips.py work/ct/all.json forela-ec2-automation
Take the RunInstances events from the Q9 addresses and sum the instances in each response. The batches were 2, 2, 2, 5, 1 and 1. Two further RunInstances calls failed and returned zero instances, so counting event lines instead of instances gives the wrong total.
python3 -I ct_runinstances.py work/ct/all.json
Gotcha: Failed
RunInstancescalls are still logged. ReadresponseElements.instancesSetof each event instead of counting lines.
Filter for CreateKeyPair events from the attacker addresses and read requestParameters.keyName. Two key pairs were created. The format mask lists the names as X.key, X, which matches one name with a .key suffix and one without.
python3 -I ct_events.py work/ct/all.json CreateKeyPair
Same CreateKeyPair events, this time the eventTime of each. The two timestamps are about thirteen hours apart, which fits the actor returning later to deploy again. Keep the order from Q4 and the ISO format with the trailing Z.
python3 -I ct_events.py work/ct/all.json CreateKeyPair --fields eventTime
The CreateKeyPair response carries the keyPairId. The plural wording is misleading: the expected answer is only the ID of 1337.key, the first key created.
python3 -I ct_events.py work/ct/all.json CreateKeyPair --fields keyName,keyPairId
Gotcha: Despite the “ID/s” wording, listing both key pair IDs does not match. Only the first key’s ID does.
CreateSecurityGroup from the attacker addresses carries the groupDescription in its request parameters.
python3 -I ct_events.py work/ct/all.json CreateSecurityGroup --fields groupName,groupDescription
Look for TerminateInstances made by the administrator identity rather than the compromised user. The first call covers the six instances from the first three launches. Truncate the eventTime to the minute for the answer format.
python3 -I ct_events.py work/ct/all.json TerminateInstances --fields eventTime,userIdentity,instanceIds
Extract the CatScale tarball, fix the directory permissions, and read the process listing. A server process runs out of /var/poshc2/money, which is the PoshC2 install path. The format mask (five characters then a 2) fits the name.
chmod -R u+rwX work/p1/catscale_out
grep -ri poshc2 work/p1/catscale_out
The PoshC2 project data sits in unusual-directory.zip, which is password protected. zip2john with rockyou did not crack it and the password is not in the collection. zipinfo shows the archive holds config-template.yml as a stored (uncompressed) entry, and that file is public in the PoshC2 GitHub repository, so a known-plaintext attack with bkcrack recovers the internal keys without the password. It needed a template from the matching era and the plaintext deflated at zlib level 9 to match the compressed bytes. With the keys, rebuild the archive without a password and read the Implants table of PowershellC2.SQLite.
zip2john unusual-directory.zip > zip.hash && john zip.hash --wordlist=/usr/share/dict/rockyou.txt
zipinfo unusual-directory.zip
bkcrack -C unusual-directory.zip -c config-template.yml -p config-template.yml.deflated -x 0
bkcrack -C unusual-directory.zip -k 93d56f14 ff7c6188 48511983 -D dec.zip
sqlite3 work/poshc2x/PowershellC2.SQLite 'select * from Implants;'
Gotcha: Two implants are registered, but only the first (the answer) is accepted. The second,
DESKTOP-JGLLJLD\admin, is not part of the expected answer, so listing both victims fails.
| Phase | Action |
|---|---|
| Credential Access | Hard-coded IAM keys read from backups/ec2.py in the world-readable forela-storage bucket |
| Initial Access | Keys for forela-ec2-automation used from four addresses in 95.181.232.0/24 |
| Defense Evasion | Key pair 1337.key created, security group created, first batches of EC2 instances launched |
| Response | Administrator terminates the first six instances |
| Defense Evasion | Second key pair 13337 created and further instances launched, 13 in total |
| Command and Control | PoshC2 server installed on an instance under /var/poshc2/money |
| Command and Control | Implants register from victim hosts, recorded in PowershellC2.SQLite |
| Type | Value |
|---|---|
| IP | 95.181.232[.]4, 95.181.232[.]8, 95.181.232[.]9, 95.181.232[.]28 |
| AWS access key ID | AKIA52GPOBQCO4SRMOWK |
| IAM user | forela-ec2-automation |
| Key pairs | 1337.key (key-0450dc836eaf2aa37), 13337 |
| Security group description | still here |
| File | backups/ec2.py (S3, forela-storage) |
| Directory | /var/poshc2/money |
| C2 victim | DESKTOP-R4KM0GJ\Marcus Athony |
| Technique | ID | Description |
|---|---|---|
| Unsecured Credentials: Credentials In Files | T1552.001 | IAM key pair hard-coded in ec2.py |
| Data from Cloud Storage | T1530 | Script retrieved from the globally readable bucket |
| Valid Accounts: Cloud Accounts | T1078.004 | Leaked keys used against the EC2 API |
| Modify Cloud Compute Infrastructure: Create Cloud Instance | T1578.002 | 13 EC2 instances launched with attacker key pairs |
| Impair Defenses: Disable or Modify Cloud Firewall | T1562.007 | Attacker-created security group |
| Application Layer Protocol: Web Protocols | T1071.001 | PoshC2 implants beaconing to the hosted server |
Never put access keys in files, and never in a world-readable bucket. The whole intrusion started with ec2.py holding a key pair in plain text. Block public access on the bucket at account level, scan stored scripts for AKIA patterns, and use instance roles instead of static keys.
Scope automation credentials tightly. The leaked user could create key pairs, security groups and launch instances from anywhere. Limit it with IAM conditions on source IP and a permission boundary, and alert on RunInstances from addresses outside the known automation range.
Alert on EC2 API calls from new source IPs. All the abuse is in CloudTrail: CreateKeyPair, CreateSecurityGroup and RunInstances from a single /24 never seen before. A rule on those three events from a new IP would have caught the first batch before the administrator spotted it.
Terminating the instances is not remediation. The administrator removed the first set, yet the keys stayed valid and the actor launched more. Deactivate the access key as the first containment step, then rotate and review.
Collect before you destroy. The CatScale triage held the process listing and the PoshC2 project database, which named the victim machines. Preserve instance data before termination so C2 infrastructure can be mapped. A ZipCrypto archive is also weak protection when a public file inside it can serve as known plaintext.
A single hard-coded key in a public bucket gave the actor full EC2 control and a place to host a PoshC2 server. CloudTrail recorded every step, so the weakness was never visibility: it was a credential that should not have been readable by anyone.