// HackTheBox  ·  writeup

Google_Cloud_Incident

HackTheBox Hard Pythonipinfo.io
Cloud

Scenario

A developer accidentally committed a Google Cloud identity’s key to a public repository.

Executive Summary

A service account key committed to a public repository was picked up and used from an external address to call the Google Cloud APIs directly. The actor first tried to create a firewall rule on the project’s network, then made repeated attempts to launch Compute Engine instances, starting in Europe. Every instance request was rejected by a GPU quota limit, so no compute was created. The whole intrusion is visible in a single exported audit log.

Artifacts

Note: The task masks hint at the answer shapes (an email address, a dotted quad, a two-word country, a single-digit count). Check them before submitting.

Initial Access

Q1 — What Google Cloud identity is compromised?

Every audit entry records who made the call in protoPayload.authenticationInfo.principalEmail. Count the distinct principals across the file; the leaked key belongs to a service account (...@<project>.iam.gserviceaccount.com), and it is the only identity making the suspicious firewall and compute calls.

python3 -I -c "import json,sys,collections as c;d=json.load(open(sys.argv[1]));print(c.Counter(e['protoPayload']['authenticationInfo']['principalEmail'] for e in d))" gcp.json
Click flag to reveal [email protected]
Q2 — What IP address is the identity authenticated from?

Pivot on the Q1 principal and read protoPayload.requestMetadata.callerIp for its events. The same address appears on every call, so there is no ambiguity about where the key was being used from.

python3 -I -c "import json,sys;d=json.load(open(sys.argv[1]));print({e['protoPayload']['requestMetadata']['callerIp'] for e in d if 'main-dev@' in e['protoPayload']['authenticationInfo']['principalEmail']})" gcp.json
Click to reveal answer 160.238.37.7
Q3 — What country does the IP originate from?

The audit log carries no geolocation, so this one needs an outside lookup of the Q2 address. ipinfo.io returned the country code KR. This answer was reasoned from that lookup rather than read from an artifact, and HTB accepted it.

curl -s https://ipinfo.io/160.238.37.7/country

Tip: Geolocation is a lookup against the caller IP, not something in the log. Do it only after the IP is confirmed from callerIp.

Click flag to reveal South Korea

Defense Evasion

Q4 — What is the name of the firewall rule the attacker attempted to create?

Filter the Q1 principal’s events to compute.firewalls.insert. The rule name sits in the request body at protoPayload.request.name. It is a generic, innocent-looking name that would blend in with the project’s built-in rules.

python3 -I -c "import json,sys;[print(e['protoPayload']['methodName'],e['protoPayload']['request'].get('name')) for e in json.load(open(sys.argv[1])) if 'firewalls.insert' in e['protoPayload']['methodName']]" gcp.json
Click to reveal answer default
Q5 — What priority was the firewall rule?

Same event as Q4, next field over: protoPayload.request.priority. Firewall priority runs from 0 (evaluated first, wins over everything) to 65535, so the value the attacker picked puts the rule ahead of every other rule on the network.

python3 -I -c "import json,sys;[print(e['protoPayload']['request'].get('priority')) for e in json.load(open(sys.argv[1])) if 'firewalls.insert' in e['protoPayload']['methodName']]" gcp.json

Gotcha: A priority of 0 is easy to mistake for a missing field in tooling that treats zero as empty. Print the raw value.

Click flag to reveal 0

Impact

Q6 — How many times did the attacker attempt to create GCE instances?

Count the compute.instances.insert events for the compromised principal. There are three attempts, and each one carries an error status rather than a completed operation.

python3 -I -c "import json,sys;print(sum('instances.insert' in e['protoPayload']['methodName'] for e in json.load(open(sys.argv[1]))))" gcp.json
Click to reveal answer 3
Q7 — What was the first region the attacker attempted to create the instances in?

Take the first of the three Q6 events and read the zone from its resource name. The attempt targeted a zone in Europe, and the answer is the zone as it appears in the log.

python3 -I -c "import json,sys;[print(e['protoPayload']['resourceName']) for e in json.load(open(sys.argv[1])) if 'instances.insert' in e['protoPayload']['methodName']]" gcp.json
Click flag to reveal europe-west1-b
Q8 — Were any of the instances created? (Yes/No)

All three instances.insert entries return the QUOTA_EXCEEDED error against the GPUS_ALL_REGIONS limit, so no instance was created. The quota name also shows what the attacker asked for: GPU-backed machines, the usual target for cryptomining abuse of stolen cloud credentials. That purpose is an inference from the quota, not something the log states.

python3 -I -c "import json,sys;[print(e['protoPayload'].get('status')) for e in json.load(open(sys.argv[1])) if 'instances.insert' in e['protoPayload']['methodName']]" gcp.json
Click to reveal answer No

Attack Summary

PhaseAction
Initial AccessService account key leaked in a public repository is used from an external address
Defense EvasionAttempts to insert a firewall rule with a generic name at the highest priority
ImpactThree attempts to create GCE instances, the first in a European zone
ImpactAll three rejected with a GPU quota error; nothing was created

IOCs

TypeValue
IP160[.]238[.]37[.]7
Identitymain-dev@cyberwox-labs[.]iam[.]gserviceaccount[.]com
Firewall rule (attempted)default, priority 0
Zone (first attempt)europe-west1-b

MITRE ATT&CK

TechniqueIDDescription
Unsecured Credentials: Credentials In FilesT1552.001Service account key committed to a public repository
Valid Accounts: Cloud AccountsT1078.004Leaked key used to call the Google Cloud APIs
Impair Defenses: Disable or Modify Cloud FirewallT1562.007Attempted firewalls.insert at priority 0
Modify Cloud Compute Infrastructure: Create Cloud InstanceT1578.002Three instances.insert attempts
Resource Hijacking: Compute HijackingT1496.001GPU quota in the failures suggests mining intent (inferred)

Defender Takeaways

Long-lived service account keys in repos are the whole incident. Secret scanning with push protection on every repository would have blocked the commit, and Google Cloud’s own leak detection can disable exposed keys. Prefer workload identity or short-lived tokens over downloaded JSON keys.

Alert on callerIp that does not match the identity’s normal sources. A developer service account calling the API from an unfamiliar country is visible in the first event. Admin Activity logs are on by default, so the detection costs nothing to build.

Treat firewalls.insert and instances.insert from a service account as high-signal. A priority 0 rule with a name that mimics a default is designed to survive a casual glance. Alert on firewall changes outside a change window and on any priority below your baseline.

Quotas were the only control that worked. The GPU limit stopped the compute abuse, not any guardrail around the key. Keep GPU and regional quotas tight and add an organisation policy restricting the resource locations and machine types a project may use.

Conclusion

A leaked service account key gave the actor full API access, and only a GPU quota stopped the attempted instance launches. The audit log showed every call, but nothing prevented the key from being used from an unfamiliar address in the first place.