AWS Security Logging

Second room in the Cloud Security for SOC module, after Cloud Security Pitfalls. That room was vocabulary; this one is the first with actual AWS logs in a SIEM, and it walks the three security areas it defined, control plane, managed services, workloads, one task at a time.

Seven tasks, sixteen answers, all solved 100%. One Splunk index and three sourcetypes:

index=* | stats count by index, sourcetype
#   aws   aws:cloudtrail            2105
#   aws   aws:cloudfront:custom      499
#   aws   aws:cloudwatch:guardduty     3

Note the shape of that: 2105 CloudTrail events, and three GuardDuty findings. CloudTrail is the raw record of every API call and it is almost entirely noise; GuardDuty is AWS’s own detection layer and it fires three times in the whole dataset. Both matter, and the room is built so that you need each one to answer questions the other cannot.

Data is dated 12 December 2025, so set the picker to All time.

Task 2: the three areas

Two definitions, and they are the module’s spine. The area covering management actions inside the AWS console is the Control Plane. The service providing lab machines in the cloud is Amazon EC2.

One small trap on the second: answering EC2 is rejected with “Your answer is too short” rather than “incorrect”, which is a helpful distinction, it means you have the right idea and the wrong string. Amazon EC2 is accepted.

Task 3: CloudTrail and the control plane

The first real query. Console logins live in CloudTrail as eventName=ConsoleLogin:

index=aws sourcetype=aws:cloudtrail jeff.harrison eventName=ConsoleLogin
| eval t=strftime(_time,"%F %H:%M:%S")
| table t, sourceIPAddress, recipientAccountId, awsRegion, "responseElements.ConsoleLogin"
#   15:26:54  149.40.62.48  398985017225  eu-north-1  Failure
#   15:39:29  149.40.62.48  398985017225  us-east-2   Success

So jeff.harrison logged in from 149.40.62.48 to account 398985017225.

Two details worth keeping. The failure and the success are thirteen minutes apart and in different regions, eu-north-1 then us-east-2, which is normal for console sign-in events and a good reminder that awsRegion on a ConsoleLogin tells you which endpoint answered, not where the user is. And responseElements.ConsoleLogin is the field carrying Success/Failure; there is no separate event name for a failed login the way there is an Event ID 4625 on-premises.

The bucket question warns you that you will need to change your query, and that is because bucket creation is not a login event:

index=aws sourcetype=aws:cloudtrail eventName=CreateBucket
| table _time, sourceIPAddress, "userIdentity.userName", "requestParameters.bucketName", awsRegion
#   15:41:09  149.40.62.48  jeff.harrison  prod-website-thm  eu-central-1

The bucket is prod-website-thm, created ninety seconds after the successful login.

Task 4: GuardDuty, and an attacker checking whether they were caught

Three findings, and reading all three in order is the whole task:

index=aws sourcetype=aws:cloudwatch:guardduty
| eval t=strftime(_time,"%H:%M:%S") | table t, Type, Severity | sort t
#   16:46:23  Discovery:IAMUser/AnomalousBehavior       sev 2
#   16:59:43  CryptoCurrency:EC2/BitcoinTool.B!DNS      sev 8
#   17:12:47  Execution:EC2/MaliciousFile               (EBS scan)

The first finding’s description names the source outright:

One or more API calls were invoked from 185.183.33.33 located in Netherlands
from ProtonVPN (ASN=49981).
GuardDuty considers these invocations suspicious because:
- The organization WorldStream B.V. (ASN=49981) has not been previously observed for this account.

The VPN is ProtonVPN. GuardDuty is doing ASN reputation here rather than geolocation, the reason given is that this organisation has never been seen for this account before, which is a far more durable signal than “unusual country”.

Now read what the alert says was actually done. The APIs invoked were ListFindings, GetDetector, ListDetectors, ListMembers, GetFindingsStatistics, ListFilters, ListOrganizationAdminAccounts, all guardduty.amazonaws.com. The attacker was enumerating GuardDuty itself, checking what detection existed and whether anything had already fired on them. GuardDuty then flagged that enumeration as Discovery-tactic anomalous behaviour. That is a genuinely nice piece of scenario design and it is the single most interesting event in the room.

The other two findings are the payload. The DNS finding carries the domain in Service.Action.DnsRequestAction:

"DnsRequestAction": { "Domain": "donate.v2.xmrig.com", "DomainWithSuffix": "xmrig.com", "Protocol": "UDP" }
"AdditionalInfo": { "threatName": "suspicious:mining/stratum" }

And the malicious-file finding is an EBS volume scan, GuardDuty snapshotted the disk and scanned 68,217 files across 2 GB:

"ThreatDetectedByName": { "ThreatNames": [{
   "Name": "CoinMiner:Linux/Xmrig.Gen", "Severity": "HIGH",
   "FilePaths": [{ "FileName": "xmrig",
                   "FilePath": "/home/ubuntu/xmrig-6.24.0/xmrig",
                   "Hash": "fb1f928c2dbfd108da2d93b9e07a8d97526dc378dc342d405f3991ad6bec969d" }] }] }

So the path is /home/ubuntu/xmrig-6.24.0/xmrig and the queried domain is donate.v2.xmrig.com.

Worth noticing that the scan reports the same hash twice, once for the extracted binary and once as xmrig-6.24.0-noble-x64.tar.gz=>xmrig-6.24.0/xmrig, the copy still inside the downloaded tarball. The => notation means “inside this archive”. Answer with the plain extracted path.

Then back to CloudTrail for instance context:

index=aws sourcetype=aws:cloudtrail eventName=RunInstances
| table _time, "userIdentity.arn", sourceIPAddress, "responseElements.instancesSet.items{}.instanceId"
#   15:17:18  arn:aws:iam::398985017225:root  78.82.223.16  i-04fa0268276e1f763
#   15:19:17  arn:aws:iam::398985017225:root  78.82.223.16  i-0cc1b6177a93db6d5

The creator is arn:aws:iam::398985017225:root, the account root, which should never be used for routine work and is the actual root cause of everything that follows.

The exposed-ports question has a query trap worth flagging. The obvious event name is AuthorizeSecurityGroupIngress, and it returns nothing. The console used a different API:

index=aws sourcetype=aws:cloudtrail eventName=ModifySecurityGroupRules
#   15:19:56  sgr-0cb6912aa908667ff  CidrIpv4 0.0.0.0/0  FromPort 22    ToPort 22    tcp
#   15:19:57  sgr-08c475641b7360c19  CidrIpv4 0.0.0.0/0  FromPort 3389  ToPort 3389  tcp

22, 3389, SSH and RDP, both open to the entire internet, one second apart, thirty-nine seconds after the instance was launched. When an event name you are confident about returns zero results in CloudTrail, search on the parameter instead (fromPort, ipPermissions) and let Splunk tell you which API actually fired; the console and the CLI often call different ones.

Task 5: managed services, CloudFront and S3 data events

CloudFront access logs are ordinary web logs, so the admin portal question is a cs_uri_stem filter:

index=aws sourcetype=aws:cloudfront:custom cs_uri_stem="/admin/*"
| table _time, c_ip, cs_method, cs_uri_stem, sc_status
#   15:39:59  168.84.119.124  POST  /admin/login      401
#   15:40:33  168.84.119.124  POST  /admin/login      200
#   15:40:59  168.84.119.124  GET   /admin/dashboard  200

168.84.119.124, one failure, then success, then the dashboard. Note this is a third address, distinct from jeff.harrison’s AWS console IP and from the ProtonVPN address; CloudFront sees the website’s visitors, CloudTrail sees the AWS account’s operators, and they are different populations.

The keyword question is a distinct count, not an event count:

index=aws sourcetype=aws:cloudfront:custom cs_uri_stem="/search" tryhackme
| stats dc(c_ip) as ips, count
#   ips 14   count 14

14 IPs, one search each, so count and dc(c_ip) agree here, but they would not have to, and dc() is the one that answers the question as asked.

Finally the S3 data events. These are eventCategory=Data in CloudTrail, and they are the record of object-level access rather than bucket-level management:

index=aws sourcetype=aws:cloudtrail eventCategory=Data
| stats count, values(eventName) as evs by "requestParameters.key"
#   admin/backup.tar.gz   3   GetObject, HeadObject, PreflightRequest
#   index.html            3   GetObject, HeadObject, PreflightRequest
#   admin/                1   PreflightRequest
#   aoc.png               1   PreflightRequest

The interesting file is backup.tar.gz. On answer format: admin/backup.tar.gz is rejected with “The answer provided may not be in English”, a validation quirk triggered by the slash rather than a wrong answer. The bare filename is accepted.

And here is the part the room does not point out, which is the best reason to run the query yourself:

index=aws sourcetype=aws:cloudtrail "requestParameters.key"="admin/backup.tar.gz"
#   16:29:55  HeadObject  185.183.33.33
#   16:29:57  GetObject   185.183.33.33

That is the ProtonVPN address from Task 4’s GuardDuty finding. The same session downloaded the admin backup at 16:29:57 and then enumerated GuardDuty at 16:30:43, take the data first, then check whether anyone noticed. The two tasks are presented as separate exercises about separate log sources, and they are the same intrusion sixteen minutes apart.

Task 6: workloads

The last two definitions. A service built and maintained by the cloud vendor is a Managed Service. And the cloud-native alternative to Auditd, which the room notes does not work effectively inside containers, is Falco, the same tool that came up in Cloud Security Pitfalls as a CWPP example.

The framing is worth keeping: if you run PostgreSQL on EC2 instead of using Aurora, or Nginx instead of an AWS load balancer, you have opted out of AWS’s logging and opted back into installing SIEM agents, deploying Auditd or Sysmon, and tracking your own vulnerabilities. Unmanaged infrastructure in the cloud is just on-premises infrastructure with someone else’s power bill.

Task 7: what to take away

Two things.

The three log sources answer three different questions, and none of them is a substitute for another. CloudTrail told us who created the instance, who opened 22 and 3389, and who pulled the backup, but it never once said “this is bad”. GuardDuty said “this is bad” three times and gave a malware path and a hash, but it could not tell us who launched the instance or who exposed the ports. CloudFront saw an entire population of users that never appears in CloudTrail at all. The room’s structure is the lesson: control plane, managed services, workloads, each with its own source.

And detection evasion now includes reading the detection tooling. The Discovery finding is not ListBuckets or GetCallerIdentity; it is ListFindings, GetFindingsStatistics, ListDetectors, an attacker with root credentials opening GuardDuty to see whether they had been caught. Any identity enumerating your security services is worth an alert on its own, because there is no benign reason for a workload to call GetFindingsStatistics, and it is one of the few actions that tells you the intruder is aware of your detection rather than merely subject to it.

Room solved 100%: seven tasks, sixteen answers, and the second of five rooms in Cloud Security for SOC.