Table of Contents

Preface Link to heading

This is a behind-the-scenes story of how I found a Service Account impersonation chain leading to vertical privilege escalation within Google SecOps SOAR.

One of the corresponding reports ended up winning the most creative award during Google Cloud bugSWAT 2025! 🎉

Note

I presented this research live during OhMyHack in Warsaw, Poland on December 2nd, 2025 (slides)


TLDR Link to heading

An authenticated attacker with the permissions sufficient to perform Manual Actions (included in the default Basic role) could’ve escalated their privileges to full Administrator access.

The root cause stemmed from overly permissive IAM bindings granted to Google-managed, per-product, per-project Service Accounts (P4SA).

Google SecOps Product Team promptly mitigated the issue (more details in the remediation section).


Live Hacking Event 101 Link to heading

Google organizes bugSWAT Live Hacking Events to facilitate collaboration between top-ranked researchers since at least 2017.

Those exhibitions are typically:

  • Invite only
  • Collaboration is encouraged
  • Bounty multipliers
  • Duplicate window
  • Predefined scope
  • Final couple days onsite

Getting invited Link to heading

I don’t think there’s any magic recipe to receive an invite other than having a track record of high signal submissions

Either way, receiving one of those invitation emails is always exciting to say the least!

Invite Email

Picking the target Link to heading

The scope for the event included nine Google Cloud Products (can’t disclose which exactly due to NDA)

Having a full-time job and other obligations, I decided to pick a single target and stick with it throughout the event.

Why Google SecOps SOAR? Link to heading

I chose Google SecOps SOAR for a few strategic reasons described below.

Why Google SecOps SOAR

Reading the docs Link to heading

Given the fact that we were waiting for per-researcher tenants to be provisioned, I decided to familiarize myself with the documentation first.

What stood out almost immediately was that Google SecOps is a single-tenant SaaS product.

This is a vital piece of information since it renders cross-tenant attack scenarios unlikely.

On top of that, the documentation provided an explicit description of the GCP-managed services in use, i.e., GKE & Cloud SQL.

archived documentation

archived documentation


Methodology Link to heading

Having received access to my Google SecOps tenant, I followed a rather simple methodology (admittedly it needs refinement).

GCP bugSWAT

Without going into details here are the two steps which I perform in cycles:

  1. Map the attack surface (unauthenticated/authenticated APIs, client-side requests, storage buckets etc.)
  2. Work on attack scenario ideation, relying only on verifiable hypotheses based on the current context, with the overarching aim of achieving maximum impact - go big or go home

SOAR Integrations Link to heading

The SecOps SOAR platform provides a way to run predefined and custom integrations.

Those are meant to provide a programmatic way to automate triage & response to security events.

Python execution environment aka RCE-as-a-Service Link to heading

One of the LHE attendees spotted an interesting excerpt in the corresponding docs.

GCP bugSWAT

archived documentation

Based on the docs, sandboxing was implemented by blocking usage of modules like os.system, several os.popen variants, and multiple functions from the subprocess module.

Right away, that denylist approach seemed insufficient.

Note

The updated documentation is far less verbose and reflects the mitigations which were put in place after the event

GCP bugSWAT

updated documentation


IDE custom code validation bypass Link to heading

Bypassing the validation mechanism turned out to be quite trivial using the dynamic __import__() method

IDE Validation Bypass

IDE Validation Bypass

We managed to set up a reverse shell by dynamically importing the necessary modules: socket, os, and pty.

The process involved using __import__('socket') to create a socket connecting back to my attacking machine, then __import__('os') to redirect standard file descriptors, and finally __import__('pty') to spawn a pseudo-terminal for an interactive shell.


We are in, what next? Link to heading

I had initial access, but none of the attempts to escalate privileges, abuse the K8s Control Plane or hit other microservices within the Cluster worked

gcloud

Based on the above, it seemed that the best bet would be to focus on the GCP Service Account bound to the K8s Pod running the Python execution environment.

Service Accounts 101

In Google Cloud, there are several different types of service accounts:

  • User-managed service accounts: Service accounts that you create and manage. These service accounts are often used as identities for workloads.
  • Default service accounts: User-managed service accounts that are created automatically when you enable certain Google Cloud services. You are responsible for managing these service accounts.
  • Service agents: Service accounts that are created and managed by Google Cloud, and that allow services to access resources on your behalf.
Service Accounts 101

https://github.com/priyankavergadia/GCPSketchnote/blob/main/images/IAMAuthorization.jpg

Workload Identity Federation for GKE

Workload Identity Federation for GKE is an elegant mechanism which provides a way to grant per-service Cloud IAM permissions within a GKE Cluster.

Workload Identity Federation for GKE

Workload Identity Federation for GKE

Note

For a deeper dive into understanding GKE Workload Identity Federation, I highly recommend to give this Medium article by Google’s Daniel Strebel a read

You might also want to learn how I was able to bypass WIF GKE Metadata Protection in specific scenarios:
https://bughunters.google.com/reports/vrp/BrQZ18W5k


Fetching the OAuth Access Token Link to heading

Once we have shell access within the container we can get the access token of the bound GCP Service Account by simply querying the Metadata Server

curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
-H "Metadata-Flavor: Google"

Here’s a demo recording of me doing exactly that.

Note

A careful reader probably spotted that gaining reverse shell access to the container isn’t a prerequisite (it’s just cool)

An insider/attacker with sufficient permissions could create & run a custom integration which would retrieve the OAuth Access Token programmatically
https://docs.cloud.google.com/compute/docs/access/authenticate-workloads#python_1


Access token introspection Link to heading

Service account access tokens are opaque, meaning I couldn’t decode them locally, but I could perform introspection using the oauth2.googleapis.com/tokeninfo API endpoint

This revealed the associated service account email:

  • gke-init-python@soar-{ID}.iam.gserviceaccount.com

Along with corresponding OAuth scopes:

  • www.googleapis.com/auth/userinfo
  • www.googleapis.com/auth/cloud-platform (broadest one there is when it comes to GCP APIs)
gcloud

What is gke-init-python used for? Link to heading

The documentation was key here. To use Workload Identity, the Google SecOps instance must be granted impersonation permissions.

The gke-init-python email identifies the unique integration that needs permission to impersonate a service account to access Google Cloud resources securely. This access is granted by adding the Service Account Token Creator role.

gcloud

Service Account impersonation Link to heading

In Google Cloud Platform (GCP), Service Account Impersonation is a security feature that allows a principal (a user or another service account) to assume the identity and permissions of a target service account temporarily.

gcloud
Note

If you come from an AWS background think of AssumeRole


Prior art Link to heading

The fact that the service account impersonation mechanism opens room for privilege escalation scenarios has been known since at least 2020.

Lateral Movement Documentation

https://docs.cloud.google.com/iam/docs/best-practices-service-accounts#lateral-movement


What can gke-init-python do? Link to heading

When I ran some rudimentary gcloud CLI commands as the gke-init-python service account, I got some surprising results.

I was able to list Service Accounts from the malachite-bugswat3007 project.

gcloud
Tip

Most Google Cloud APIs expose the testIamPermissions() method
It can be used to enumerate given service account’s permissions
Reference: docs


Malachite enters the scene Link to heading

Malachite is the internal name for Chronicle, now known as SecOps SIEM.

My conjecture was that the gke-init-python could impersonate other service accounts within the Malachite project, most likely because it had been assigned the iam.serviceAccountTokenCreator role project-wide.

docs

docs

I quickly validated that hypothesis by:

  1. Running the gcloud auth print-access-token command as gke-init-python with the –impersonate-service-account flag set to one of the target SAs
SECOPS_AUTH_TOKEN=$(gcloud auth print-access-token --impersonate-service-account=bugswat3007-secops-auth@malachite-bugswat3007.iam.gserviceaccount.com)
  1. Loading the corresponding token as the specific env variable CLOUDSDK_AUTH_ACCESS_TOKEN expected by gcloud
export CLOUDSDK_AUTH_ACCESS_TOKEN=$SECOPS_AUTH_TOKEN
  1. Verifying that token at hand is valid

The above led me to the following conclusion, which I reported early on.


Revised architecture diagram Link to heading

I updated my mental model of the architecture, noting the separation between the SOAR environment (where gke-init-python lived) and the malachite-bugswat{id} project.

Architecture Diagram

Architecture Diagram


Auth flow is complex Link to heading

Out of the four Service Accounts that I could impersonate, it was secops-auth that caught my attention.

I followed the intuition and dug into the complex authentication flow, capturing all the HTTP(S) traffic during the standard sign-in process.

Auth Flow Diagram

Let’s take a closer look at what happens after the session cookie is set:

  • in step #7 a POST request is made to generateSoarAuthJwt endpoint, retrieving a signed JWT (let’s call it SOAR_SIGNED_JWT)
  • in step #8 the SOAR_SIGNED_JWT is exchanged for a yet another, siemplify specific signed JWT (let’s call it SIEMPLIFY_SIGNED_JWT)

Examining SOAR_SIGNED_JWT Link to heading

When I examined a SOAR JWT payload, I noticed that its issuer was the secops-auth Service Account (bugswat3007-secops-auth@malachite-bugswat3007.iam.gserviceaccount.com)!

Issuer
How can a Service Account sign JWTs?

Let’s take a step back to understand how a Service Account could sign JWTs.

Each GCP Service Account comes with a Google-managed key pair

A service account can sign a JWT using its system-managed private key by invoking the projects.serviceAccounts.signJwt method

Sign JWT

Sign JWT

The subsequent signature can then be verified using the corresponding public key available in the following formats:


Connecting the dots Link to heading

This was the moment the whole chain clicked for me:

  1. I could execute API calls using the permissions of the gke-init-python Service Account
  2. gke-init-python had the permissions to impersonate the secops-auth Service Account
  3. secops-auth Service Account was the confirmed issuer of SOAR_SIGNED_JWT

Let’s sign our own JWTs! Link to heading

An attacker could abuse the gke-init-python Service Account to impersonate the JWT issuer (secops-auth) and sign a forged JWT with arbitrary claims!

Forge JWT

Forge JWT

Note

If you want to try this yourself, check out the jdomeracki/service-account-impersonation-demo project on GitHub, which provides a supplementary demo showcasing this scenario in action


Vertical Privilege Escalation Link to heading

Mario

While the primitive was obviously interesting it didn’t automatically follow that it would be highly impactful

Let’s assume that only Admins could get their hands on the gke-init-python Service Account OAuth Access Token via the IDE custom code validation bypass.
In that case, having the ability to mint arbitrary JWTs wouldn’t significantly broaden the prerequisite level of access.
Taking that into consideration, my updated goal was to find pathways to abuse gke-init-python as a lower-privileged user.

Tip

When assessing privesc scenarios, Google VRP takes a permission delta approach

The larger the increase in access, the larger the impact and thus the reward

The default Basic user group is granted the permission to perform Manual Actions, and so I began looking for ways to abuse that minimal level of access.

Privilege Escalation Diagram

Full SSRF via the HTTPv2 integration Link to heading

One of the Google-managed integrations that looked ripe for exploitation was the HTTPv2 integration

It provides the ability to set:

  • An arbitrary URL (http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token)
  • HTTP Method (GET)
  • Arbitrary HTTP headers (Metadata-Flavor: Google)

What’s more, the corresponding code didn’t block access to link-local address range 169.254.0.0/16 (most likely by design).

An attacker could easily exploit this SSRF to fetch the OAuth Access Token bound to the Python execution environment Service Account.

SSRF Service Account Access Token

Code Execution via SSTI in TemplateEngine PowerUp Link to heading

Another pathway to get the Access Token I found was via a classic Jinja2 Server Side Template Injection (SSTI) in the TemplateEngine PowerUp.

I won’t go into details—suffice to say that exploiting this SSTI was easy because subprocess.Popen was already imported.

Jinja Payload

Here’s a PoC recording.


Full attack scenario Link to heading

Putting it all together, the attack scenario looked like this:

  1. An authenticated attacker would perform a malicious action (SSTI or SSRF) to fetch the OAuth Access Token of the gke-init-python Service Account.
  2. They would sign a forged JWT, abusing the Service Account impersonation chain (gke-init-python impersonates secops-auth), resulting in a SOAR_SIGNED_JWT.
  3. Finally, they would exchange the forged SOAR_SIGNED_JWT for a SIEMPLIFY_SIGNED_JWT, achieving full administrative access.
Full Attack Scenario

Full Attack Scenario


Impact assessment Link to heading

Google SecOps is an Enterprise offering often used by heavily regulated companies (Pharma, Finance, etc.).

A typical ratio of SIEM/SOAR Admins to Analysts would probably be at least 1:10 (don’t quote me on that, numbers made out of thin air).

Given the recent proliferation of fake candidates circumventing background checks and APTs targeting large organizations from within, the risk of a malicious insider abusing this or similar privilege escalation pathways is non-trivial.


The aftermath Link to heading

I reported the above from Lufthansa’s WiFi while I was flying over to the on-site part of the event (past duplicate window).

What followed was quite unexpected—according to one of Google’s representatives, they already received a submission a few months back which highlighted the initial components of the chain.

The Google VRP Team conducted an internal investigation and concluded that there’s no impact; hence, no reward was given to the original reporter.

Not only that, other bugSWAT attendees also reported related issues, but those were initially deemed as informational.

The reported privesc chain proved high severity impact, so all researchers were rewarded as a result! 💵💵💵
(Needless to say, I got quite a few thumbs up and taps on the back :D)

Note

According to Google VRP Panel members the original reporter was rewarded retroactively as well - I think it shows a great level of maturity!

I then presented a live exploitation demo during Show&Tell and ended up winning the most creative reward! 🎉

Remediation Link to heading

Google SecOps Team promptly remediated the risk by removing the project-wide Service Account Token Creator binding which was granted to the SOAR Python pod service account.

Remediation

What’s more, the SecOps SOAR platform is currently undergoing a significant migration to a new per-customer tenant model.

Note

Below is a more detailed explanation which I received from one of the Google VRP representatives:
“As part of a long-term change, we’ve shifted to a new model where a single P4SA is provided to SOAR, replacing the previous project-level grant.
This P4SA is used for impersonation by all SOAR services including the Python. The Chronicle API P4SA provides a set of permissions that administrators can manage, and the Python service is configured to use only the essential subset of these permissions required for its operations.

This new flow introduces three key security benefits:
1.Granular, Pre-defined Permissions,
2.Adhering to best-practice in Google Cloud.
3.Customer-Managed Access: A key benefit of the P4SA is that it places control in the hands of the administrator. They can audit, modify, and approve the permissions granted to SOAR, ensuring the principle of least privilege is enforced according to their organization’s standards.”


Closing Remarks Link to heading

Google Cloud bugSWAT 2025 was an incredible experience, and I’m grateful for the opportunity to collaborate with fellow researchers and the Google VRP Team!

GCP bugSWAT