HIGH AWS S3 Server Access Logs Data Exfiltration LTR-0016 ✓ validated

Detect Anonymous S3 Object Reads and Malicious Uploads in Server Access Logs

Published

MITRE ATT&CK: T1530T1608.001

Sigma Rule

title: S3 Anonymous Object Read and Script Upload
id: c9b48814-efc5-4f0c-9d57-2eca393911d2
status: experimental
description: >
  Detects two high-signal events in S3 server access logs. First, a successful
  unauthenticated object download - the requester field is ANONYMOUS and the
  operation REST.GET.OBJECT returned 200, which is only possible when the bucket
  or object grants public access. Against a bucket that should be private, this
  is confirmed data exposure, not a probe. Second, an upload of an executable
  script into the bucket, the staging pattern used to plant payloads on
  infrastructure that trusts bucket contents.
references:
  - https://attack.mitre.org/techniques/T1530/
  - https://attack.mitre.org/techniques/T1608/001/
author: LogTriage
date: 2026/09/18
logsource:
  product: aws
  service: s3
detection:
  anonymous_object_read:
    requester: 'ANONYMOUS'
    operation: 'REST.GET.OBJECT'
    http_status: 200
  script_upload:
    operation: 'REST.PUT.OBJECT'
    key|endswith:
      - '.sh'
      - '.exe'
      - '.ps1'
  condition: anonymous_object_read or script_upload
falsepositives:
  - Intentionally public buckets (static sites, download mirrors) - scope to private buckets
  - CloudFront origin fetches without Origin Access Control signing
  - CI/CD tooling uploading bootstrap scripts to an artifacts bucket (allowlist the role ARN)
level: high
tags:
  - attack.collection
  - attack.t1530
  - attack.resource_development
  - attack.t1608.001

What this rule detects

S3 server access logs record one line per request, and the third-from-key field — the requester — is the one that matters most. A real principal shows up as an IAM ARN; an unauthenticated request shows up as ANONYMOUS. This rule fires on:

  1. ANONYMOUS + REST.GET.OBJECT + 200 — an unauthenticated download that succeeded. That is only possible when the bucket grants public access, so against a bucket that should be private, this is confirmed data exposure — not an attempt.
  2. REST.PUT.OBJECT of a script (.sh, .exe, .ps1) — the staging pattern for planting a payload in a bucket that other infrastructure trusts and executes from.

Detection logic

The anonymous-read selection is a strict conjunction, and each conjunct removes an ambiguity:

  • requester: ANONYMOUS — no credentials were presented; this cannot be a confused-deputy or over-permissioned role, because there is no role.
  • operation: REST.GET.OBJECT — object content was requested, not bucket metadata like REST.GET.VERSIONING (which appears in normal console traffic).
  • http_status: 200 — the bytes were served. A 403 here would be reconnaissance; the 200 is what turns it into an incident.

The upload selection is narrower than “any PUT” on purpose. Writes are the normal business of a bucket; writes of executable scripts are not, outside dedicated artifacts buckets. An attacker with write access uses exactly this to stage payloads where EC2 user-data scripts, CI jobs, or scheduled tasks will fetch and run them.

There is no count threshold on either selection — one anonymous read of a private object is one incident.

Validated against a real sample

Validated against s3_access_attack.log (shipped with LogTriage), three requests against corp-data-bucket:

  • 185.220.101.42 as ANONYMOUS pulls confidential/customers.csv — REST.GET.OBJECT, status 200, 5,242,880 bytes sent, user-agent python-requests/2.28.0
  • The same IP, still ANONYMOUS, pulls confidential/employees.csv a minute later — 200, 1,048,576 bytes
  • Then 45.142.212.100, authenticated as arn:aws:iam::123456789:user/attacker, does a REST.PUT.OBJECT of malware/backdoor.sh via boto3/1.26.0

That sequence reads as scripted exfiltration of ~6 MB of confidential data from a publicly readable bucket, followed by a compromised or attacker-created IAM user staging a script in the same bucket.

The rule fires on that sample and stays silent on the matching benign log, where every request carries a real requester ARN (arn:aws:iam::123456789:user/alice, .../bob), the user-agents are S3Console/0.4 and aws-sdk-java/1.11.975, and the only PUT is uploads/report-2024-01.pdf — no ANONYMOUS requester and no script keys anywhere.

False positives

The big one is definitional: buckets that are supposed to be public. A static website bucket or a public download mirror serves ANONYMOUS GETs as its entire job. Scope this rule to buckets tagged private — better yet, drive the scope from your S3 inventory rather than from exclusions inside the rule.

CloudFront origin fetches without Origin Access Control signing also appear anonymous; the fix is the OAC configuration, not a rule exclusion. For the upload selection, CI/CD pipelines that publish bootstrap.sh-style scripts to an artifacts bucket are the recurring benign case — allowlist that pipeline’s requester ARN for that bucket, and keep the selection live everywhere else.

Frequently Asked Questions

What does ANONYMOUS in the requester field actually prove?
That the request carried no AWS credentials at all — no SigV4 signature, no IAM principal, nothing. S3 only serves such a request when the bucket policy, an ACL, or a public-access setting explicitly allows it. So an ANONYMOUS + 200 pair proves two things at once: the bucket is publicly readable, and someone unauthenticated is actively reading it. There is no 'maybe' in this signal — the only question is whether the bucket was meant to be public.
Why alert on the 200 success rather than on 403 AccessDenied probes?
Both are worth watching, but they mean different things. A burst of 403s is reconnaissance — someone enumerating bucket names or testing permissions, which is constant background noise on AWS. A 200 is exposure: the object's bytes actually left. This rule deliberately keys on the confirmed case because it demands immediate response (block public access, inventory what was read), whereas 403 probing merits a lower-severity enumeration rule with a count threshold.
How do S3 server access logs differ from CloudTrail data events for this detection?
Server access logs are free, request-level, and include the requester, operation, key, status, and bytes sent — everything this rule needs — but delivery is best-effort and can lag by hours. CloudTrail S3 data events are billed per event, delivered reliably, and structured as JSON. The detection logic is identical in both: in CloudTrail, ANONYMOUS reads appear as GetObject with no userIdentity ARN. If you can afford data events on sensitive buckets, run the rule on both feeds.
What is the response playbook when the anonymous-read selection fires?
First, stop the bleeding: enable Block Public Access at the bucket (or account) level, which overrides any policy or ACL granting public reads. Second, scope the exposure: the same access log tells you exactly which keys were fetched, by which IPs, and how many bytes were sent — in the sample, two confidential CSVs totalling 6 MB. Third, treat the contents as compromised: rotate any credentials or tokens inside the exposed objects and notify per your data-handling obligations. Only then investigate how the bucket became public.

Related Resources

See this detection run on a real report

Try the live demo with a pre-loaded malicious log set — no signup required — or upload your own log file and get a full AI-reviewed threat report in minutes.