// CyberDefenders  ·  writeup

GoogleCloudHunt

CyberDefenders jq

Scenario

In today’s cloud-driven environments, security incidents can pose significant risks, particularly when involving unauthorized access to critical resources. This lab walkthrough focuses on investigating a potential credential breach within a Google Cloud Platform (GCP) environment. Acting as a cybersecurity analyst, your task is to trace the attacker’s actions, uncover evidence of malicious activity, and identify the methods used to gain access and maintain persistence. The lab simulates real-world attack scenarios providing an opportunity to strengthen cloud forensic and investigation skills.

Executive Summary

A single Google Cloud principal showed an anomalous volume of authentication events compared to every other account in the environment, marking it as the compromised credential behind this incident. From that foothold, the attacker enumerated and accessed a Cloud Storage bucket holding sensitive documents, returning repeatedly to one specific file in a pattern consistent with exfiltration. The same principal then reached into two Compute Engine instances that shared a single attached service account for their API calls. Using that access, the attacker attempted to export a Cloud SQL database to an external storage bucket — an operation Cloud IAM blocked on a missing permission. Unable to exfiltrate the database directly, the attacker pivoted to persistence, creating a new service account and generating a private key for it to maintain covert, credential-based access going forward.

Artifacts

Tooling note: Every question in this lab is solved by progressively narrowing jq filters on protoPayload fields — authenticationInfo.principalEmail, resourceName, serviceName, methodName, and authorizationInfo[]. Each prior answer becomes the pivot value for the next filter.

Initial Access
Q1 — We need to determine if a user has fallen victim to the credential breach, which user account was compromised?

GCP Cloud Audit Logs record every authenticated API call, so a compromised credential should surface as anomalous access volume rather than needing external threat intel. Pull every principal email out of the log file and rank by frequency — a compromised account under active attacker use will dwarf normal user activity.

# tool: jq
jq '.[] | .protoPayload.authenticationInfo.principalEmail' logs.json | sort | uniq -c | sort -nr

One address sits far above the rest in the ranked output — a volume of activity no legitimate user generates in this timeframe, and the account this entire investigation centers on for every subsequent question.

Click flag to reveal david.smith8392173781@gmail.com
Discovery
Q2 — In the attacker's initial exploration of the environment, what is the name of the first Google Cloud Storage bucket they accessed?

With the compromised principal confirmed, narrow the logs to resource access events referencing storage buckets rather than reviewing every log line — resourceName is the field that carries this, and filtering on it directly isolates every bucket interaction in the dataset.

# tool: jq
jq '.[] | select(.protoPayload.resourceName != null and (.protoPayload.resourceName | contains("buckets"))) | {bucket: .protoPayload.resourceName, user: .protoPayload.authenticationInfo.principalEmail}' logs.json

One bucket appears first in the output and repeatedly thereafter, every hit tied to the compromised principal — the attacker’s first target once inside the environment.

Click flag to reveal projects/_buckets/confidential-documents-482374561
Q4 — Knowing each step the attacker takes is crucial. What's the name of the Compute Engine instance the attacker accessed?

Compute Engine access from the same compromised principal needs a different filter — scope to the compute.googleapis.com service and specifically to authorizationInfo entries referencing instance resources, rather than every API call the principal made.

# tool: jq
jq '.[] | select(.protoPayload.authenticationInfo.principalEmail == "david.smith8391273718@gmail.com" and .protoPayload.serviceName == "compute.googleapis.com" and .protoPayload.authorizationInfo[].resource? != null and (.protoPayload.authorizationInfo[].resource | contains("/instances/"))) | .protoPayload.authorizationInfo[].resource' logs.json

Two distinct instance names repeat across the filtered output — the attacker didn’t stop at one machine, touching both with sustained, repeated access rather than a single opportunistic connection.

Click to reveal answer monitoring-instance and analytics-instance
Q5 — What service account is used by the Compute instance for calls to Google Cloud APIs?

Compute instances authenticate to Google Cloud APIs through an attached service account rather than a user identity — filter for log entries carrying serviceAccountDelegationInfo, a field that only populates when a service account is doing the calling on the instance’s behalf.

# tool: jq
jq '.[] | select(.protoPayload.authenticationInfo.serviceAccountDelegationInfo != null) | {instance: .protoPayload.resourceName, serviceAccount: .protoPayload.authenticationInfo.principalEmail}' logs.json

The same service account address ties back to both instances identified in Q4, confirming a single shared identity handling API calls for both — one credential worth targeting for persistence rather than two.

Click flag to reveal cloudops-service@hybrid-elixir-370815.iam.gserviceaccount.com
Collection
Q3 — Considering the attacker's focus on data exfiltration, what's the name of the object they may have exfiltrated within the first accessed bucket?

Rather than list every object in the bucket identified in Q2, scope the filter specifically to that bucket’s path and pull the object names referenced inside it — repeated access to a single object, versus one-off browsing, is what marks exfiltration rather than reconnaissance.

# tool: jq
jq '.[] | select(.protoPayload.resourceName != null and (.protoPayload.resourceName | test("/buckets/confidential-documents-482374561/"))) | {bucket: (.protoPayload.resourceName | split("/")[3]), object: .protoPayload.resourceName}' logs.json

One file name recurs across multiple access events tied to the compromised principal — the object the attacker returned to repeatedly, consistent with staging it for exfiltration rather than a single incidental read.

Click flag to reveal Financial_Report_2023_Classified.pdf
Exfiltration
Q6 — In the process of exfiltrating data, identify the Google Cloud SQL database the attacker attempted to export. What is the database's name?

With the compromised principal already confirmed, scope the filter to the cloudsql.googleapis.com service directly rather than searching broadly — Cloud SQL activity from that principal narrows straight to the database in question.

# tool: jq
jq '.[] | select(.protoPayload.authenticationInfo.principalEmail == "david.smith8391273718@gmail.com" and .protoPayload.serviceName == "cloudsql.googleapis.com") | {database: .protoPayload.resourceName, user: .protoPayload.authenticationInfo.principalEmail}' logs.json

One database name repeats throughout the filtered output, tied consistently to the compromised principal — the attacker’s target for the exfiltration attempt that follows.

Click flag to reveal analytics-db
Q7 — Tracking the data movement, which Google Cloud Storage bucket did the attacker attempt to export the database to?

Export attempts leave a trace even when they fail — filter for the specific cloudsql.instances.export permission against the database identified in Q6, and pull both the resource URI and any status message the API returned.

# tool: jq
jq '.[] | select(.protoPayload.resourceName != null and (.protoPayload.resourceName | contains("analytics-db")) and (.protoPayload.authorizationInfo[].permission == "cloudsql.instances.export")) | {uri: .protoPayload.resourceName, message: .protoPayload.status.message}' logs.json

The output names the destination bucket the attacker targeted for the export, alongside a status message confirming the operation failed — the attacker had the database targeted, but lacked the storage.objects.create permission needed to actually write the export out.

Gotcha: A failed operation still fully discloses attacker intent in the logs — don’t skip entries just because status.message shows an error.

Click flag to reveal backup-repository-543268313682
Persistence
Q8 — In the attacker's effort to maintain access, identify the new service account created by the attacker. What is the account ID of this service account?

Blocked from exfiltrating the database directly, the attacker’s next move is establishing persistence — filter for the CreateServiceAccount IAM method specifically, which fires exactly once per new service account and names both the creator and the new account ID.

# tool: jq
jq '.[] | select(.protoPayload.methodName == "google.iam.admin.v1.CreateServiceAccount") | {createdBy: .protoPayload.authenticationInfo.principalEmail, serviceAccountId: .protoPayload.request.account_id}' logs.json

The output confirms the compromised service account itself created a brand-new service account — a textbook cloud persistence move that survives even if the original compromised user credential gets rotated.

Click flag to reveal cloud-ops-service
Q9 — As part of establishing persistence, what is the ID of the secret key generated for the newly created service account?

A new service account alone doesn’t grant remote access — it needs a key. Filter for the CreateServiceAccountKey method to find exactly when, and for which account, a key was minted.

# tool: jq
jq '.[] | select(.protoPayload.methodName == "google.iam.admin.v1.CreateServiceAccountKey") | {createdBy: .protoPayload.authenticationInfo.principalEmail, serviceAccountId: .protoPayload.response.name}' logs.json

The response.name field returns the key ID generated for the service account created in Q8 — the credential that gives the attacker standing, covert access to the environment independent of the originally compromised user.

Click flag to reveal 16569af4fb98194fc210770d353a2513bada24b3

Attack Summary

PhaseAction
Initial AccessCompromised GCP principal identified via anomalous authentication volume across cloud audit logs
DiscoveryAttacker enumerated and accessed a Cloud Storage bucket holding sensitive documents
CollectionRepeated access to a specific document within the bucket, consistent with staging for exfiltration
DiscoveryAttacker accessed two Compute Engine instances sharing a common service account
DiscoveryShared service account identified as the identity used for API calls from both instances
ExfiltrationAttacker attempted to export a Cloud SQL database using the shared service account’s access
ExfiltrationExport attempt targeted an external storage bucket but failed due to missing IAM permission
PersistenceAttacker created a new service account to maintain access independent of the original credential
PersistenceSecret key generated for the new service account to enable covert, ongoing API access

IOCs

TypeValue
Accountdavid.smith8392173781@gmail.com
Storage Bucketprojects/_buckets/confidential-documents-482374561
FileFinancial_Report_2023_Classified.pdf
Compute Instancemonitoring-instance
Compute Instanceanalytics-instance
Service Accountcloudops-service@hybrid-elixir-370815.iam.gserviceaccount.com
Databaseanalytics-db
Storage Bucketbackup-repository-543268313682
Service Accountcloud-ops-service
Secret Key ID16569a4f4b09194fc21077d353a2513bada24b3

MITRE ATT&CK

TechniqueIDDescription
Valid Accounts: Cloud AccountsT1078.004Attacker operated using a single compromised GCP principal for all initial access
Cloud Infrastructure DiscoveryT1580Attacker enumerated Cloud Storage buckets and Compute Engine instances via authenticated API calls
Data from Cloud StorageT1530Attacker repeatedly accessed and staged a sensitive object within a Cloud Storage bucket
Exfiltration to Cloud StorageT1567.002Attacker attempted to export a Cloud SQL database to an attacker-designated storage bucket
Create Account: Cloud AccountT1136.003Attacker created a new service account using the compromised identity’s IAM permissions
Account Manipulation: Additional Cloud CredentialsT1098.001Attacker generated a secret key for the newly created service account to maintain persistent access

Defender Takeaways

Per-principal authentication volume is the single strongest signal in this incident, and it’s cheap to monitor continuously. No malware signature or IOC feed would have caught this — only a baseline of expected API call volume per identity, with alerting on outliers. Any principal generating an order-of-magnitude more authenticated events than its historical norm deserves immediate review, especially personal-domain email addresses operating alongside enterprise identities in the same project.

A single service account shared across multiple Compute Engine instances multiplies blast radius unnecessarily. Compromise of one instance’s credentials granted the attacker equivalent access on a second, unrelated instance purely because both were configured to authenticate as the same identity. Scoping service accounts per-instance or per-workload, following least privilege, would have contained the compromise to a single machine.

cloudsql.instances.export and storage.objects.create are exactly the kind of narrow, high-impact IAM permissions that warrant real-time alerting regardless of outcome. The export attempt in this case failed only because the attacker’s service account lacked one specific permission — a coincidence of misconfiguration, not a designed control. Alert on every export attempt against production databases, successful or not, rather than relying on IAM to silently absorb the failure.

google.iam.admin.v1.CreateServiceAccount and CreateServiceAccountKey are rare, high-signal events that should never pass without review. Legitimate service account creation is infrequent and usually tied to a change-management ticket. Any unattributed creation of a new service account, followed immediately by a key generation for that same account, is close to a definitive persistence indicator and should trigger automated alerting, not just periodic audit review.

Conclusion

The attacker never needed to escalate privileges or pivot through a network — a single compromised cloud credential was enough to browse storage, ride a shared service account across compute instances, and attempt a database export. The one control that actually stopped data from leaving the environment was a narrowly scoped IAM permission the attacker’s service account didn’t have; every other layer of this environment let the compromise walk straight through.

I successfully completed GoogleCloudHunt Blue Team Lab at @CyberDefenders! https://cyberdefenders.org/blueteam-ctf-challenges/achievements/inksec/googlecloudhunt/

#CyberDefenders #CyberSecurity #BlueYard #BlueTeam #InfoSec #SOC #SOCAnalyst #DFIR #CCD #CyberDefender