API Connections as an Unintended Proxy

▶ Watch (01:18)

During a customer engagement, Gulbrandsrud noticed unusual symbols in an Azure Logic App and pulled the raw HTTP response for an API connection. It contained a testLinks URI pointing at Azure Resource Management. Calling it as a reader returned a live Slack channel list, with the request executing under the connection’s stored credential. The Slack do-not-disturb triggered on an unknown account (05:33) sealed it: someone else got snoozed, not him.

Why ARM’s Security Model Makes This Possible

▶ Watch (10:16)

Azure’s security model gives readers permission to issue any GET request through ARM. The bigger issue: ARM doesn’t just forward the caller’s token. After the role check passes, it queries the backend with its own administrator-level credential. This is by design, not accident. Gulbrandsrud showed a historical example where this model failed: the getFunctionsAdminToken endpoint was a straight copy from Kudu, a GET request, and fully accessible to readers. It returned remote code execution on the web app.

What Readers Can Actually Read

▶ Watch (13:54)

The testLinks endpoint proxied requests to the backend service using the connection’s stored credential. For Slack that’s a channel list. But API connections exist for Azure SQL, Key Vault, and Jira. An SQL connection let any reader query all table values. A Key Vault connection exposed every stored secret and key. A Jira connection leaked the full API key because Jira’s connector requires the target URL as a request header, and the response included it in plaintext.

“it is filled with bugs” – Haakon Gulbrandsrud

Path Traversal Across Tenants via Dynamic Invoke

▶ Watch (17:42)

After the extension/proxy path was whitelisted and the bounty denied, Gulbrandsrud looked at a second endpoint from the original documentation diagram: dynamic/invoke. It accepts a POST body with a method and path field. As a POST, it requires contributor access. But it has no path validation. ARM checks the path against the swagger, then does a string join to build the APIM request URI. Supplying a path with %2e%2e (dot-dot) sequences lets a contributor walk backward out of their own connection and into another tenant’s.

Cross-Tenant Key Vault Exfiltration

▶ Watch (22:27)

Gulbrandsrud built a custom connector in his attacker tenant with a swagger path that accepts a string parameter. The victim tenant had a Key Vault API connection with a known connection ID. Using the cross-tenant Key Vault secret read live demo (23:17), he path-traversed from his own connection ID into the victim’s, listed all secrets, and read plaintext values. Microsoft patched in two days and paid $40,000. The fix blacklisted %2e in paths. Any alternate traversal technique still qualifies for a bounty.

“they concluded that uh I get 40,000” – Haakon Gulbrandsrud

Q&A

How do the logs look in the victim’s account when you read the Key Vault secret? The logs show the API connection itself accessing the Key Vault, which looks normal because the same connection fires whenever the logic app runs, making the attack indistinguishable from legitimate traffic. ▶ 26:29

Notable Quotes

it is filled with bugs Haakon Gulbrandsrud · ▶ 4:56

that’s fine I guess but you have no way Haakon Gulbrandsrud · ▶ 25:50

they concluded that uh I get 40,000 Haakon Gulbrandsrud · ▶ 24:10

Key Takeaways

  • Any reader in an Azure tenant can call the test-link URI on API connections, executing requests under the stored credential.
  • Azure Resource Manager silently proxies backend calls with its own admin token after only a role-level GET check.
  • Microsoft patched both bugs quietly with no CVE, leaving affected tenants no way to know if they were compromised.