Detect Anonymous S3 Object Reads and Malicious Uploads in Server Access Logs
Published
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:
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.REST.PUT.OBJECTof 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 likeREST.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.42asANONYMOUSpullsconfidential/customers.csv—REST.GET.OBJECT, status 200, 5,242,880 bytes sent, user-agentpython-requests/2.28.0- The same IP, still
ANONYMOUS, pullsconfidential/employees.csva minute later — 200, 1,048,576 bytes - Then
45.142.212.100, authenticated asarn:aws:iam::123456789:user/attacker, does aREST.PUT.OBJECTofmalware/backdoor.shviaboto3/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.