// HackTheBox  ·  writeup

MisCloud

HackTheBox Medium tsharkPython
Cloud

Scenario

The e-commerce startup “DummyExample” experienced a data breach of sensitive user data hosted on Google Cloud. Investigate who is behind the breach and how it happened.

Executive Summary

An internal Windows workstation logged into a self-hosted Gitea server running an old, vulnerable release and edited a repository’s post-receive git hook through the web UI. The hook ran a bash reverse shell to an ngrok tunnel, giving the actor a shell on the Gitea instance. From there the attached default Compute Engine service account was used against Cloud Storage and OS Login, and a dedicated storage service account read the customer-data workbook. A request to make the bucket public with allUsers was denied, so the actor encrypted the file with a repeating-key XOR and served it over HTTP on the RDP port, which the default VPC rules already allowed. The audit log and the packet capture together show every step.

Artifacts

Format note: Q4 expects IP:insider or IP:outsider, Q6 expects the machine names comma-separated in alphabetical order, and Q12 expects SSN:CardNumber.

Initial Access

Q1 — What's the private IP address of the Windows machine?

Start with conversation statistics on the capture to find the internal hosts. One private address carries Edge and Google update traffic (gvt1), takes an RDP session from an external address, and browses the Gitea web UI as the user James. That is the Windows workstation; the Linux and mirror instances do not browse the web.

tshark -r VPCNetwork_Traffic.pcap -q -z conv,ip
tshark -r VPCNetwork_Traffic.pcap -Y http.request -T fields -e ip.src -e http.host -e http.user_agent
Click flag to reveal 10.128.0.3
Q2 — Which CVE was exploited by the threat actor?

Pull the HTTP requests to the Gitea server (port 3000). The page footer reports Gitea 1.2.0, and the interesting traffic is a git-receive-pack push followed by a web-UI edit of a repository’s post-receive git hook. Gitea 1.2.0 lets any repository owner edit server-side git hooks from the settings page, which is arbitrary command execution as the Gitea service user. That is the CVE-2020-14144 pattern. The version and the hook edit are the evidence; the CVE number itself comes from the NVD entry for Gitea’s git-hook feature rather than from anything in the capture.

tshark -r VPCNetwork_Traffic.pcap -Y 'http.request' -T fields -e frame.number -e ip.src -e http.request.method -e http.request.uri
Click to reveal answer CVE-2020-14144

Execution

Q3 — What is the hostname and port number to which the reverse shell was connecting?

Follow the stream that carries the hook POST (/James/Test-Learn/settings/hooks/git/post-receive). The form body carries the hook script base64-encoded. Decoded, it is a bash one-liner that redirects an interactive shell to /dev/tcp/ on an ngrok TCP tunnel in the EU region, backgrounded so the push does not hang.

tshark -r VPCNetwork_Traffic.pcap -Y 'http.request.method==POST && http.request.uri contains "post-receive"' -T fields -e http.file_data
echo '<base64 from hook>' | base64 -d

Tip: Decode the base64 and the host and port are right there in the /dev/tcp/ redirect.

Click flag to reveal 0.tcp.eu.ngrok.io:14509
Q4 — From which IP address was the CVE exploited, and is this threat an insider or outsider attack?

Check the source of the hook POST, not the source of the login. The POST to post-receive comes from the Windows workstation found in Q1, an internal address, so the exploit was run from inside the VPC and the threat is an insider. A second external address does log in as John and push code, but it never touches the hook settings. The format mask expects IP:insider/outsider.

tshark -r VPCNetwork_Traffic.pcap -Y 'http.request.method==POST && http.request.uri contains "hooks/git"' -T fields -e ip.src -e ip.dst -e http.request.uri

Gotcha: The external IP that authenticates as John and pushes is a decoy for this question. It looks like the attacker, but the exploitation request originates from the internal Windows host, so “outsider” with that IP is wrong.

Click to reveal answer 10.128.0.3:insider

Credential Access

Q5 — Which account helped the threat actor to pivot?

The reverse shell lands on the Gitea Linux instance, which sits in GCP with the default Compute Engine service account attached. In GCloud_Logs.json, filter the audit entries by protoPayload.authenticationInfo.principalEmail and look for the default compute service account ([email protected]) calling the API from the external address 34.66.191.87. It tries storage.buckets.getIamPolicy on the data bucket and gets denied, which is the pivot out of the VM into cloud APIs.

python3 -I -c "import json,collections as c;d=json.load(open('GCloud_Logs.json'));print(c.Counter((e['protoPayload']['authenticationInfo']['principalEmail'],e['protoPayload']['methodName'],e['protoPayload'].get('status',{}).get('code',0)) for e in d))"
Click flag to reveal [email protected]

Lateral Movement

Q6 — Which machines did the threat actor log into? (sorted alphabetically)

From Q5’s account, list the OS Login CheckPolicy calls. Each call names the instance the service account was authorising a login to. Three instances appear, sorted alphabetically below. These are the hosts the actor logged into, not every instance in the project.

python3 -I -c "import json;[print(e['protoPayload']['resourceName']) for e in json.load(open('GCloud_Logs.json')) if 'CheckPolicy' in e['protoPayload']['methodName']]"
Click to reveal answer linux-machine1,linux-machine2,packet-mirror-instance

Collection

Q9 — Which account led to the cloud storage data breach?

Look at storage.objects.get calls on the customer-data object in GCloud_Logs.json. A dedicated storage service account performs the successful read, which is the account behind the data leaving the bucket. It is distinct from the compute account in Q5, whose bucket access was denied.

python3 -I -c "import json;[print(e['protoPayload']['authenticationInfo']['principalEmail'],e['protoPayload']['methodName'],e['protoPayload'].get('status',{}).get('code',0)) for e in json.load(open('GCloud_Logs.json')) if e['protoPayload']['methodName'].startswith('storage.')]"
Click flag to reveal storage-svc-acc@qwiklabs-gcp-00-848c1b920007.iam.gserviceaccount.com
Q7 — What's the original name of the sensitive file?

The same storage.objects.get entry carries the object’s real resourceName. The attacker’s copy on the wire is cusdata.xlsx.enc, but the bucket object is the original customer data workbook with a long hash suffix. Copy the name from the log field exactly.

python3 -I -c "import json;[print(e['protoPayload']['resourceName']) for e in json.load(open('GCloud_Logs.json')) if e['protoPayload']['methodName']=='storage.objects.get']"

Tip: Answer with the bucket object name from the audit log, not the renamed file the attacker served.

Click flag to reveal Customer-Data-e7b9e806c08435793e310d7137b068fa.xlsx

Exfiltration

Q8 — Which gcloud role did the threat actor try to assign to the storage bucket to make it publicly accessible?

Filter storage.objects.update and bucket IAM calls for policyDelta. One request tries to ADD allUsers with a read role to make the data public. The status code on the entry is 9 (failed precondition), so the change was denied, but the role in the request is what the question asks for.

python3 -I -c "import json;[print(e['protoPayload']['serviceData']['policyDelta']['bindingDeltas']) for e in json.load(open('GCloud_Logs.json')) if 'policyDelta' in json.dumps(e)]"
Click to reveal answer roles/storage.legacyObjectReader
Q10 — Which port number was exploited by the attacker to exfiltrate data that is allowed by default ingress traffic rules in the default VPC network?

With the bucket route blocked, the actor staged the file on the compromised Linux host and served it over HTTP. tshark does not decode HTTP on that port by default, which is why Wireshark’s export-objects missed it. Find the TCP conversation that carries a Python SimpleHTTP response with /cusdata.xlsx.enc going to the external RDP address. The port is the one the default VPC ingress rules already allow: RDP.

tshark -r VPCNetwork_Traffic.pcap -q -z conv,tcp | sort -k9 -rn | head
tshark -r VPCNetwork_Traffic.pcap -d tcp.port==3389,http -Y http.request.uri contains cusdata -T fields -e ip.src -e ip.dst -e tcp.dstport

Hint: The default VPC network allows ingress on 22, 3389 and ICMP. The big transfer rides one of them even though it is not that protocol.

Click to reveal answer 3389

Defense Evasion

Q11 — What is the key to decrypt the encrypted file?

Follow the large TCP stream, strip the HTTP headers, and write the body to cusdata.xlsx.enc. The file is not a valid zip. The ciphertext shows a repeating keystream with a 36-byte period, so it is a repeating-key XOR. Recover the key bytes from the keystream, then pick the rotation so the output starts with the zip magic PK\x03\x04. The key was recovered by known-plaintext reasoning against the zip header; the only ambiguity was which rotation of the 36 characters to write down, and the one aligned to file offset 0 is the readable passphrase.

python3 -I extract_stream.py VPCNetwork_Traffic.pcap 126 cusdata.xlsx.enc
python3 -I xor_dec.py cusdata.xlsx.enc cusdata.xlsx   # try each key rotation, keep the one producing PK\x03\x04

Gotcha: The rotation of the 36-byte key matters. A wrong rotation produces garbage that still looks structured, so check for the PK header, not just entropy.

Click flag to reveal J@m37_h@Rd3st_k3Y_enCrypt_Exf!l7r@73

Impact

Q12 — What are the SSN and credit card numbers of "Founder John"?

Open the decrypted workbook and search every sheet for the Founder John row. The SSN column holds the value in the HTB placeholder format and the next column holds the card number. Submit them joined with a colon.

python3 -I find_row.py cusdata.xlsx 'Founder John'
Click to reveal answer HTB-FR-SRLK:1111-3345-1234-5123

Remediation

These three are cloud-hardening questions answered from Google Cloud product documentation, not from the evidence.

Q13 — Which Google Cloud service is an alternative to Gitea?

Gitea is a self-hosted Git service, and Google Cloud’s managed private Git hosting is Cloud Source Repositories. This is reference knowledge, not something read from an artifact, and the answer is reasoned rather than observed.

Click flag to reveal Cloud Source Repositories
Q14 — Is it safe to use the Default Compute Engine Service Account on VM instances?

The default Compute Engine service account is created with the broad Editor role in many projects, and the attacker pivoted through it in Q5. Google’s guidance is to create dedicated, least-privilege service accounts per workload instead. This is reasoned from that guidance.

Click to reveal answer No
Q15 — Which Google Cloud service restricts data exfiltration from Cloud Storage?

The control that sets a perimeter around Google APIs such as Cloud Storage and blocks exfiltration to resources outside it is VPC Service Controls. Reference knowledge.

Click flag to reveal VPC Service Controls

Attack Summary

PhaseAction
Initial AccessInternal Windows workstation logs into Gitea 1.2.0 over the VPC and pushes to a test repository
Executionpost-receive git hook edited in the web UI to run a bash reverse shell (CVE-2020-14144)
Command and ControlShell connects out to an ngrok TCP tunnel
Credential AccessDefault Compute Engine service account used from the Gitea VM; bucket getIamPolicy denied
Lateral MovementOS Login policy checks for three instances
CollectionStorage service account reads the customer-data workbook
ExfiltrationAttempt to grant allUsers read on the bucket is denied
ExfiltrationWorkbook XOR-encrypted and served over HTTP on port 3389 to the external RDP host

IOCs

TypeValue
IP (insider workstation, hook POST)10.128.0[.]3
IP (external, RDP/John login)169.150.196[.]101, 154.198.108[.]200
IP (audit log, compute SA)34.66.191[.]87
Host0[.]tcp[.]eu[.]ngrok[.]io:14509
Filecusdata.xlsx.enc
Account[email protected]
Accountstorage-svc-acc@qwiklabs-gcp-00-848c1b920007.iam.gserviceaccount.com

MITRE ATT&CK

TechniqueIDDescription
Exploit Public-Facing ApplicationT1190Gitea 1.2.0 git-hook editing (CVE-2020-14144)
Unix ShellT1059.004Bash reverse shell in the post-receive hook
Valid Accounts: Cloud AccountsT1078.004Compute and storage service accounts used for cloud API access
Remote Services: SSHT1021.004Logins to Linux instances via OS Login
Data from Cloud StorageT1530storage.objects.get on the customer workbook
Exfiltration Over Alternative ProtocolT1048HTTP served on the RDP port
Obfuscated Files or InformationT1027Repeating-key XOR of the workbook

Defender Takeaways

Patch or retire self-hosted Git. Gitea 1.2.0 lets a repository owner write server-side hooks from the web UI, which is code execution as the service user. Upgrade, disable user-editable git hooks, and alert on POSTs to settings/hooks/git.

Treat internal users as a threat model. The exploit came from a workstation inside the VPC, so perimeter-only controls never saw it. Log and review administrative actions on internal developer tools.

Stop attaching the default Compute Engine service account. One compromised VM gave the actor a service account usable against Cloud Storage and OS Login. Use per-workload service accounts with least privilege and watch for getIamPolicy denials as a recon signal.

Block egress, not just ingress. Data left over port 3389 because the default VPC rules allow it inbound. Remove the default allow rules, restrict egress, and put Cloud Storage inside a VPC Service Controls perimeter.

Alert on public-access attempts. A denied allUsers binding is still a signal that someone is trying to expose a bucket. Alert on policyDelta entries containing allUsers or allAuthenticatedUsers.

Conclusion

An insider used an outdated Gitea server to reach cloud credentials and walked a customer workbook out of the project, even after the public-bucket attempt failed. The data left on a port the default network rules already trusted.