Cloud persistence hunting¶
Enumerating and investigating persistence mechanisms implanted at the cloud control-plane level: IAM entities, compute backdoors, storage manipulation, CI/CD pipeline modification, and cloud function abuse.
AWS: enumerate unexpected IAM state¶
# generate a credential report (includes all users and their credential state)
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d | column -t -s ','
# look for users with no console access but active access keys
# (common pattern for backdoor service accounts)
aws iam list-users --output json | python3 -c "
import json, sys
from datetime import datetime, timezone
users = json.load(sys.stdin)['Users']
for u in users:
last_used = u.get('PasswordLastUsed', 'never')
print(f\"{u['UserName']:40} created: {u['CreateDate']} last_password: {last_used}\")
"
# check for recently attached policies (unusual privilege escalation)
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AttachRolePolicy \
--start-time "$(date -d '30 days ago' --iso-8601=seconds)" \
--output json | python3 -c "
import json, sys
events = json.load(sys.stdin)['Events']
for e in events:
detail = json.loads(e['CloudTrailEvent'])
params = detail.get('requestParameters', {})
print(e['EventTime'], e.get('Username','?'),
params.get('roleName','?'), params.get('policyArn','?'))
"
AWS: Lambda and cloud functions¶
Lambda functions can be used as persistent execution platforms. They run on AWS infrastructure rather than a host you control, survive host reimaging, and can be triggered by many event sources.
# list all Lambda functions
aws lambda list-functions \
--query 'Functions[*].[FunctionName,Runtime,LastModified,Role]' \
--output table
# look for functions modified outside deployment windows
aws lambda list-functions --output json | python3 -c "
import json, sys
from datetime import datetime
funcs = json.load(sys.stdin)['Functions']
for f in funcs:
print(f['LastModified'], f['FunctionName'], f['Runtime'])
" | sort
# review the execution role of any suspicious function
aws lambda get-function-configuration --function-name FUNCTION_NAME \
--query '[Role, Environment.Variables]'
A Lambda function with an execution role that has broad IAM permissions, or one with environment variables containing credentials, should be reviewed carefully.
AWS: SSM Parameter Store and Secrets Manager¶
Parameters modified outside the change management process can be a persistence vector if applications read them on startup.
# list all parameters with their last-modified date
aws ssm describe-parameters \
--query 'Parameters[*].[Name,LastModifiedDate,LastModifiedUser,Type]' \
--output table | sort -k2
# check CloudTrail for PutParameter events
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=PutParameter \
--start-time "$(date -d '7 days ago' --iso-8601=seconds)" \
--query 'Events[*].[EventTime,Username,Resources[0].ResourceName]' \
--output table
AWS: S3 bucket policy changes¶
Bucket policies modified to allow public access or cross-account access can be used to stage payloads or exfiltrate data persistently.
# check for buckets with public access
aws s3api list-buckets --query 'Buckets[*].Name' --output text |
tr '\t' '\n' | while read bucket; do
result=$(aws s3api get-bucket-acl --bucket "$bucket" \
--query 'Grants[?Grantee.URI==`http://acs.amazonaws.com/groups/global/AllUsers`]' \
--output text 2>/dev/null)
[[ -n "$result" ]] && echo "PUBLIC ACL: $bucket -- $result"
done
# check CloudTrail for bucket policy changes
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=PutBucketPolicy \
--start-time "$(date -d '30 days ago' --iso-8601=seconds)" \
--output table
Azure: function apps and logic apps¶
# list all function apps
az functionapp list --query '[*].[name,resourceGroup,state,lastModifiedTimeUtc]' -o table
# check for function apps with recently modified code
# (use deployment history rather than az cli for code content)
az functionapp deployment list-publishing-profiles --name FUNCNAME \
--resource-group RG --query '[*].[publishMethod,lastModified]'
CI/CD: GitHub Actions¶
Pipeline definitions are a persistence vector when the pipeline carries cloud credentials. A step added to an existing workflow will execute with those credentials on every pipeline run.
# list recent workflow file changes (git-based approach on the repo)
git log --all --name-only --format='%H %ae %ad' -- '.github/workflows/**'
# or via GitHub API
gh api repos/ORG/REPO/commits \
--jq '.[] | select(.files[]?.filename | startswith(".github/workflows/")) |
{sha: .sha, author: .commit.author.email, date: .commit.author.date,
message: .commit.message}'
# show the diff of a specific workflow change
gh api repos/ORG/REPO/commits/COMMIT_SHA \
--jq '.files[] | select(.filename | startswith(".github/workflows/")) | .patch'
When reviewing a modified workflow, look for:
New steps with unusual names (
Cache dependency validation,Environment setup)Steps that run shell commands referencing
$AWS_*,$AZURE_*,$TOKEN, or similar environment variablesSteps that use
|| trueto suppress error outputSteps calling external URLs not matching known CDN or tool providers
CI/CD: secrets access audit¶
# GitHub: list secrets and their last-updated date
gh api repos/ORG/REPO/actions/secrets \
--jq '.secrets[] | {name: .name, updated_at: .updated_at}'
# check Actions audit log for secret access (requires org audit log API)
gh api orgs/ORG/audit-log \
--paginate \
--jq '.[] | select(.action | startswith("secret")) | {action, actor, repo, created_at}'
Cloud detection correlation¶
Effective cloud persistence hunting requires correlating multiple signals:
Signal |
What it indicates |
|---|---|
IAM user created, no corresponding Jira/ServiceNow ticket |
Potential backdoor account |
Lambda function modified outside deployment window |
Code injection or new function |
SSM parameter modified, application restarts spike shortly after |
Startup-hook persistence |
GitHub workflow modified, new external URL contacted in next pipeline run |
CI/CD injection |
Service principal added to privileged group, no change request |
Identity persistence |
Bucket policy changed, new GetObject requests from external IP |
Data staging or exfiltration |
Build detections around these correlations rather than individual events.
A single IAM user creation may be legitimate; an IAM user creation followed
within 24 hours by an access key creation and an AssumeRole call from an
external IP is a pattern worth escalating.