In this scenario, players are tasked with identifying and investigating insider threats by analyzing various artifacts, while also learning how to conduct an AWS incident analysis.
A victim was talked into running a remote-access tool and sharing its connection ID over IRC, which let an outside operator take control of the workstation. From the desktop the operator read an AWS access key out of a Sticky Notes database and used it from their own IP against a single S3 bucket. CloudTrail shows the operator suspending bucket versioning to remove the rollback path, requesting expedited restores of archived objects, and attempting to delete the largest data file, which was denied. They finished by overwriting objects with corrupted copies uploaded in the original storage class so the change would look routine. The workstation triage and the CloudTrail log together cover the whole chain.
access.log, Prefetch, the Sticky Notes plum.sqlite-wal, registry hives → grep, strings, PECmd, regipywork/ct.jsonl) and filtered with PythonTimestamp note: The remote-access tool logs in the workstation’s local time (UTC-8). CloudTrail is UTC. Any answer asking for UTC from a workstation log needs converting first.
The IceChat logs hold the private conversation between the victim and the operator. In the Query folder, the log for 2024-11-13 shows the victim typing a number in a spaced-out format at 04:20. That is the remote-access client ID, and it is submitted as digits only.
grep -riE "[0-9]{2,3} [0-9]{3} [0-9]{3}" IceChat/Logs/Query/
Gotcha: The ID is typed with spaces in the chat, but the answer format is eight characters, so strip them.
The remote-access tool left its own directory on disk, and Prefetch confirms it actually ran. Parse the matching .pf file with PECmd for the executable name and last run time. The run time of 12:07 UTC lines up with the chat conversation, which also pins down the host timezone in Q4.
PECmd -f AA_V3.EXE-2B85CCB4.pf
The tool’s access.log records the inbound connection, but in local time (04:23:34). From Q3, Prefetch shows the first run at 12:07 UTC while the chat shows the same moment at 04:07, so the host sits at UTC-8 and the connection is eight hours later in UTC. This offset was reasoned from that correlation rather than read from the registry, because regipy could not return the TimeZoneInformation key from the SYSTEM hive.
cat access.log
.venv/bin/python3 -I -c "import sys; from regipy.registry import RegistryHive; print(RegistryHive(sys.argv[1]).get_key('ControlSet001\\Control\\TimeZoneInformation').get_values())" SYSTEM
Gotcha: The raw
access.logtime is the local clock. The date is the same either way, but the hour is wrong for a UTC answer.
Every CloudTrail event made with the stolen key carries the same sourceIPAddress, and no other address appears for that access key. The same address also appears next to the operator’s nickname in a WHOIS line in the IRC logs.
python3 -I -c "import json,collections as c; print(c.Counter(json.loads(l).get('sourceIPAddress') for l in open('work/ct.jsonl')))"
Access key IDs start with AKIA and are 20 characters long. The collection includes the Sticky Notes plum.sqlite database, whose recent edits sit in the write-ahead log, so run strings over the -wal file and match the key pattern. The same ID is the principal in the CloudTrail events, which ties the stolen key to the S3 activity.
strings -n 8 plum.sqlite-wal | grep -oE "AKIA[A-Z0-9]{16}"
Filter the merged CloudTrail file to the key from Q5 and count the bucketName request parameter. One bucket accounts for all the activity.
python3 -I -c "import json,collections as c; print(c.Counter((json.loads(l).get('requestParameters') or {}).get('bucketName') for l in open('work/ct.jsonl') if 'AKIA52GPOBQCBFYGAYHI' in l))"
Object-level events (GetObject, RestoreObject, PutObject) carry the object key in requestParameters.key. The first path segment of every key is the main directory.
python3 -I -c "import json,collections as c; print(c.Counter(((json.loads(l).get('requestParameters') or {}).get('key') or '').split('/')[0] for l in open('work/ct.jsonl')))"
Versioning is controlled by PutBucketVersioning. The request body sets the status to Suspended, and CloudTrail eventTime is already UTC.
python3 -I -c "import json; [print(e['eventTime'], e['eventName'], e['requestParameters']) for e in map(json.loads, open('work/ct.jsonl')) if e['eventName']=='PutBucketVersioning']"
Suspending versioning stops S3 keeping prior object versions, which removes the victim’s ability to roll back later overwrites and deletes. ATT&CK files that under Inhibit System Recovery.
Gotcha: T1562 (Impair Defenses) looks like the natural fit for “turning off a protection”, but the question wants the recovery-focused technique.
The objects sit in an archive storage class, so reading them first needs RestoreObject. List every RestoreObject event in order. The earliest attempts carry an error and started nothing; the first event without an errorCode is the successful one.
python3 -I -c "import json; [print(e['eventTime'], e.get('errorCode'), (e.get('requestParameters') or {}).get('key')) for e in map(json.loads, open('work/ct.jsonl')) if e['eventName']=='RestoreObject']"
Gotcha: The first
RestoreObjectcalls fail withKeyTooLongError. Skip them.
The RestoreObject request body includes a RestoreRequest with a Tier field. It names the retrieval option.
python3 -I -c "import json; [print((e.get('requestParameters') or {}).get('RestoreRequest')) for e in map(json.loads, open('work/ct.jsonl')) if e['eventName']=='RestoreObject']"
A DeleteObject event with an AccessDenied error code marks an attempt that did not succeed. It was made at 16:04:09 UTC, and the key in the request is the target.
python3 -I -c "import json; [print(e['eventTime'], e.get('errorCode'), e['requestParameters']) for e in map(json.loads, open('work/ct.jsonl')) if e['eventName']=='DeleteObject']"
CloudTrail has no object size field, but the GetObject events for the Q12 file record bytesTransferredOut. Summing them gives 159383552 bytes, which is 152 MiB (159383552 / 1048576). The size was derived from transfer counts rather than read from a bucket listing.
python3 -I -c "import json; print(sum(json.loads(l)['additionalEventData']['bytesTransferredOut'] for l in open('work/ct.jsonl') if '\"GetObject\"' in l and 'GiftList_Worldwide.csv' in l))"
Tip: Copy the byte count as raw text and divide by 1048576 once at the end. The format wants an integer.
Replacement shows up as PutObject calls into the targeted prefix. Sort the PutObject events by time and take the earliest.
python3 -I -c "import json; print(sorted(e['eventTime'] for e in map(json.loads, open('work/ct.jsonl')) if e['eventName']=='PutObject')[0])"
The PutObject request parameters include x-amz-storage-class. The corrupted uploads reuse the archive class of the originals.
python3 -I -c "import json,collections as c; print(c.Counter(((json.loads(l).get('requestParameters') or {}).get('x-amz-storage-class')) for l in open('work/ct.jsonl') if '\"PutObject\"' in l))"
Tip: The value in CloudTrail is upper case. The answer format is lower case.
| Phase | Action |
|---|---|
| Command and Control | Victim shares a remote-access client ID over IRC and runs the tool |
| Command and Control | Operator connects to the workstation |
| Credential Access | AWS access key read from the Sticky Notes database |
| Collection | Stolen key used from the operator’s IP against one S3 bucket |
| Impact | Bucket versioning suspended |
| Impact | Expedited restores of archived objects |
| Impact | Delete of the large CSV attempted and denied |
| Impact | Objects overwritten with corrupted files in the original storage class |
| Timestamp (UTC) | Event |
|---|---|
| 2024-11-13 12:07 | Remote-access tool executed on the workstation (Prefetch) |
| 2024-11-13 12:23:34 | Operator connects to the workstation |
| 2024-11-13 15:31:15 | PutBucketVersioning sets versioning to suspended |
| 2024-11-13 15:43:49 | First successful RestoreObject |
| 2024-11-13 16:04:09 | DeleteObject on the CSV denied |
| 2024-11-13 16:10:03 | First PutObject overwrite |
| Type | Value |
|---|---|
| IP | 146.70.202[.]35 |
| Executable | AA_V3.EXE |
| Remote-access client ID | 95192516 |
| AWS access key | AKIA52GPOBQCBFYGAYHI |
| S3 bucket | arctic-archive-freezer |
| S3 prefix | Claus_Operation_Data |
| Targeted object | GiftList_Worldwide.csv |
| Technique | ID | Description |
|---|---|---|
| Remote Access Software | T1219 | Remote-access tool run by the victim and used for the operator’s session |
| Unsecured Credentials: Credentials In Files | T1552.001 | AWS key stored in a Sticky Notes database |
| Valid Accounts: Cloud Accounts | T1078.004 | Stolen key used from an external IP |
| Data from Cloud Storage | T1530 | GetObject and RestoreObject against the bucket |
| Inhibit System Recovery | T1490 | Bucket versioning suspended |
| Data Destruction | T1485 | Attempted delete of the large CSV |
| Data Manipulation: Stored Data Manipulation | T1565.001 | Objects overwritten with corrupted files |
Long-lived access keys do not belong in a sticky note. The key sat in plain text on a workstation and worked from any IP. Use short-lived role credentials, and scan endpoints for AKIA strings.
Alert on PutBucketVersioning with a suspended status. It is rare and high-signal, and here it came well before the destructive steps. Pair it with MFA Delete or an SCP that denies it, and keep Object Lock on critical buckets.
Watch the key’s source IP. All the malicious activity came from one external address. An IP condition on the access key policy, or a GuardDuty finding for unusual use, would have flagged it.
Treat archive restores and overwrites as events. RestoreObject on an archive tier followed by PutObject matching the original storage class is deliberate. Data-event logging on the bucket is what made this reconstruction possible.
Remote-access tools run by users need controls. The victim shared a connection ID in chat and handed over the desktop. Application control on unapproved remote-access tools and user training close that entry point.
The operator went from a chat message to a tampered S3 bucket in a few hours, using only a key from a sticky note and one remote-access session. The sharpest lesson is the order of operations: versioning was suspended before anything was restored, deleted or overwritten, so that first event was the warning.