ApertaCookie.psm1
using namespace "System.Security.Cryptography" <# .SYNOPSIS Copies specified cookies sqlite database to temp directory .DESCRIPTION Copies specified cookies sqlite database to temp directory to avoid issues related to a browser having a file lock on the original database. .EXAMPLE Copy-CookieDBToTemp -SQLitePath $pathToCookieDB Copies browser cookie SQLite DB to temp dir location. .PARAMETER SQLitePath Path of SQLite Cookie DB to copy .OUTPUTS System.String -or- System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ This is a necessary action to prevent issues where the browser has a lock on the orignal databse. In testing I noticed that Linux was especially sensitive to this. Although I also saw the freeze behavior on Windows when querying FireFox. This copy action shifts ApertaCookie to querying the copy instead of the primary database. .COMPONENT ApertaCookie #> function Copy-CookieDBToTemp { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Path of SQLite Cookie DB to copy')] [string] $SQLitePath ) $tempDir = [System.IO.Path]::GetTempPath() $tempPath = $tempDir + 'apertacookie' Write-Verbose -Message ('Evaluating temp location: {0}' -f $tempPath) if (-not(Test-Path -Path $tempPath)) { Write-Verbose -Message 'Creating ApertaCookie directory in temp location...' try { $newItemSplat = @{ ItemType = 'Directory' Force = $true Path = $tempPath ErrorAction = 'Stop' } New-Item @newItemSplat | Out-Null Write-Verbose -Message 'Temp directory created.' } catch { Write-Error $_ return $false } } else { Write-Verbose -Message 'Temp path confirmed.' } Write-Verbose -Message 'Initiating Cookie DB copy...' Write-Verbose -Message ('Copying from: {0}' -f $SQLitePath) Write-Verbose -Message ('Copying to: {0}' -f $tempPath) try { $copySplat = @{ Path = $SQLitePath Destination = $tempPath Force = $true Confirm = $false PassThru = $true ErrorAction = 'Stop' } $results = Copy-Item @copySplat $fullName = $results.FullName } catch { Write-Error $_ return $false } Write-Verbose -Message ('Returning full copy path: {0}' -f $fullName) return $fullName } #Copy-CookieDBToTemp <# .SYNOPSIS Retrieves the FireFox profile that has been written to most recently .DESCRIPTION FireFox can have multiple profiles. This function finds the profile that has been written to most recently. This is likely a good indicator of the active (primary) profile. .EXAMPLE Get-FireFoxProfilePath -Path $pathToFireFoxProfiles Returns the FireFox profile path that has been written to most recently. .PARAMETER Path Path to FireFox Profiles .OUTPUTS System.String -or System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ This isn't perfect. Open to better suggestions. .COMPONENT ApertaCookie #> function Get-FireFoxProfilePath { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Path to FireFox Profiles')] [string] $Path ) Write-Verbose -Message ('Evaluating {0}' -f $Path) try { $itemSplat = @{ Path = $Path ErrorAction = 'Stop' } $recentProfile = Get-ChildItem @itemSplat | Where-Object { $_.PSIsContainer -and $_.Name -match '\.default' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 Write-Verbose $recentProfile } catch { Write-Error $_ return $false } Write-Verbose -Message ('Most recent FireFox profile: {0}' -f $recentProfile.FullName) return $recentProfile.FullName } #Get-FireFoxProfilePath <# .SYNOPSIS Returns the correct path to the SQLite Cookie database and SQLite Table Name based on OS and Browser .DESCRIPTION Evaluates the provided browser and OS and returns the known location for the SQLite Cookie Database and SQLite Table Name .EXAMPLE Get-OSCookieInfo -Browser 'Chrome' Returns SQLite Cookie Database path for OS that function is run on as well as SQLite Table Name .PARAMETER Browser Browser choice .OUTPUTS System.Management.Automation.PSCustomObject .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ .COMPONENT ApertaCookie #> function Get-OSCookieInfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Browser choice')] [ValidateSet('Edge', 'Chrome', 'FireFox')] [string] $Browser ) Write-Verbose -Message ('Browser: {0}' -f $Browser) if ($IsWindows) { Write-Verbose -Message 'Windows Detected' switch ($Browser) { Edge { $sqlPath = "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Cookies" $tableName = 'cookies' } Chrome { $sqlPath = "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Cookies" $tableName = 'cookies' } FireFox { # TODO: check database lock. you may have to copy the file $profilePath = Get-FireFoxProfilePath -Path "$env:APPDATA\Mozilla\Firefox\Profiles" if ($profilePath) { $sqlPath = "$profilePath\cookies.sqlite" $tableName = 'moz_cookies' } else { # TODO: ERROR CONTROL } } } } #if_Windows elseif ($IsLinux) { Write-Verbose -Message 'Linux Detected' switch ($Browser) { Edge { $sqlPath = "$env:HOME/.config/microsoft-edge-beta/Default/Cookies" $tableName = 'cookies' } Chrome { $sqlPath = "$env:HOME/.config/google-chrome/Default/Cookies" $tableName = 'cookies' } FireFox { $profilePath = Get-FireFoxProfilePath -Path "$env:HOME/.mozilla/firefox" if ($profilePath) { $sqlPath = "$profilePath/cookies.sqlite" $tableName = 'moz_cookies' } else { # TODO: ERROR CONTROL } } } } #elseif_Linux elseif ($IsMacOS) { Write-Verbose -Message 'OSX Detected' switch ($Browser) { Edge { $sqlPath = "$env:HOME/Library/Application Support/Microsoft Edge/Default/Cookies" $tableName = 'cookies' } Chrome { $sqlPath = "$env:HOME/Library/Application Support/Google/Chrome/Default/Cookies" $tableName = 'cookies' } FireFox { $profilePath = Get-FireFoxProfilePath -Path "$env:HOME/Library/Application Support/Firefox/Profiles" if ($profilePath) { $sqlPath = "$profilePath/cookies.sqlite" $tableName = 'moz_cookies' } else { # TODO: ERROR CONTROL } } } } #elseif_OSX Write-Verbose -Message ('SQLite Path: {0}' -f $sqlPath) Write-Verbose -Message ('Table Name: {0}' -f $tableName) $obj = [PSCustomObject]@{ SQLitePath = $sqlPath TableName = $tableName } return $obj } #Get-OSCookieInfo <# .SYNOPSIS Retrievs the primary cookie decryption key for Edge or Chrome cookies .DESCRIPTION Queries the Local State and uses the currentuser context to decrypt the cookies key which can be used to decrypt cookies. This decrypt context can then be loaded into an AesGcm in the context of $Script:GCMKey .EXAMPLE Get-WindowsCookieDecryptKey -Browser Edge Retrieves the cookie decryption key using the currentuser context for Edge .PARAMETER Browser Browser choice .OUTPUTS System.Byte .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ This was very hard to make. $Script:GCMKey should be used for decrypting cookies .COMPONENT ApertaCookie #> function Get-WindowsCookieDecryptKey { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Browser choice')] [ValidateSet('Edge', 'Chrome', 'FireFox')] [string] $Browser ) Write-Verbose -Message ('Browser: {0}' -f $Browser) switch ($Browser) { Edge { $statePath = "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Local State" } Chrome { $statePath = "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" } FireFox { Write-Verbose -Message 'No state decryption needed for FireFox. Skipping' return } } Write-Verbose -Message ('Retrieving content from {0}' -f $statePath) try { $contentSplat = @{ Path = $statePath ErrorAction = 'Stop' } $cookiesKeyEncBaseSixtyFourRaw = Get-Content @contentSplat } catch { Write-Error $_ return $false } Write-Verbose -Message 'Converting from JSON...' $cookiesKeyEncBaseSixtyFour = ($cookiesKeyEncBaseSixtyFourRaw | ConvertFrom-Json -AsHashtable).'os_crypt'.'encrypted_key' Write-Verbose -Message 'Converting from Base64...' $cookiesKeyEnc = [System.Convert]::FromBase64String($cookiesKeyEncBaseSixtyFour) | Select-Object -Skip 5 # Magic number 5 Write-Verbose -Message 'Running Unprotect...' try { $cookiesKey = [System.Security.Cryptography.ProtectedData]::Unprotect($cookiesKeyEnc, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser) } catch { Write-Error $_ return $false } return $cookiesKey } #Get-WindowsCookieDecryptKey <# .SYNOPSIS Creates System.Security.Cryptography.AesGcm with provided key byte .DESCRIPTION Creates System.Security.Cryptography.AesGcm with provided key byte from the decrypted Local State .EXAMPLE New-WindowsAesGcm -WinDecrypt $key .PARAMETER WinDecrypt Windows Local State Key .OUTPUTS System.Bool .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ This was very hard to make. $Script:GCMKey should be used for decrypting cookies Some of this code was inspired from Read-Chromium by James O'Neill: https://www.powershellgallery.com/packages/Read-Chromium/1.0.0/Content/Read-Chromium.ps1 .COMPONENT ApertaCookie #> function New-WindowsAesGcm { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( [Parameter(Mandatory = $true, HelpMessage = 'Windows Local State Key')] [System.Object] $WinDecrypt ) Write-Verbose -Message 'Creating AesGcm...' try { $Script:GCMKey = [AesGcm]::new($WinDecrypt) Write-Verbose -Message 'Decrypt Key loaded into script variable!' } catch { Write-Error $_ return $false } return $true } #New-WindowsAesGcm <# .SYNOPSIS Takes in cookie encrypted byte encrypted_value and returns decrypted cookie value .DESCRIPTION Using the GCMKey decrypt key returns decrypted cookie value from a provided cookie encrypted_value byte value .EXAMPLE Unprotect-Cookie -Cookie $cookies[0].encrypted_value -Verbose Decrypts the provided cookie byte .PARAMETER Cookie Encrypted cookie value in byte format .OUTPUTS System.String .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ Use GCM decrytpion if ciphertext starts "V10" & GCMKey exists, else try ProtectedData.unprotect Requires Get-CookieDecryptKey to have already been run to load $Script:GCMKey Some of this code was inspired from Read-Chromium by James O'Neill: https://www.powershellgallery.com/packages/Read-Chromium/1.0.0/Content/Read-Chromium.ps1 .COMPONENT ApertaCookie #> function Unprotect-Cookie { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Encrypted cookie value in byte format')] [System.Object] $Cookie ) Write-Verbose -Message 'Decrypting cookie...' try { if ($Script:GCMKey -and [string]::new($Cookie[0..2]) -match "v1\d") { Write-Verbose -Message 'AesGcm decrypt' #Ciphertext bytes run 0-2="V10"; 3-14=12_byte_IV; 15 to len-17=payload; final-16=16_byte_auth_tag $output = [System.Byte[]]::new($Cookie.length - 31) # same length as payload. #_____________________________________________________________________________________________ # https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.aesgcm?view=net-5.0 $Script:GCMKey.Decrypt( $Cookie[3..14], $Cookie[15..($Cookie.Length - 17)], $Cookie[-16..-1], $output, $null) [string]::new($output) #_____________________________________________________________________________________________ } else { Write-Verbose -Message 'Attempting CurrentUser decryption method' [string]::new([ProtectedData]::Unprotect($Cookie, $null, 'CurrentUser')) } } catch { Write-Warning $_ } } #Unprotect-Cookie <# .EXTERNALHELP ApertaCookie-help.xml #> function Convert-CookieTime { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Chrome Based Time')] [Int64] $CookieTime, [Parameter(HelpMessage = 'Specify switch if converting from FireFox time')] [switch] $FireFoxTime ) # 01/01/1601 00:00:00 - Chrome time date to vet against # 13267233550477440 - time input to expect # 01/01/1970 00:00:00 - firefox time date to vet against # 1616989552356002 - firefox time input Write-Verbose -Message 'Converting cookie time to dateTime!' if ($FireFoxTime) { Write-Verbose -Message 'FireFox detected. Taking us back to 1970! Groovy!' [datetime]$baseTime = '01/01/1970 00:00:00' } else { Write-Verbose -Message 'Taking us back to 1601!' [datetime]$baseTime = '01/01/1601 00:00:00' } $seconds = $CookieTime / 1000000 [dateTime]$convertedTime = $baseTime.AddSeconds($seconds) return $convertedTime } #Convert-CookieTime <# .EXTERNALHELP ApertaCookie-help.xml #> function Get-DecryptedCookiesInfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Browser choice')] [ValidateSet('Edge', 'Chrome', 'FireFox')] [string] $Browser, [Parameter(Mandatory = $false, HelpMessage = 'Domain to search for in Cookie Database')] [string] $DomainName, [Parameter(HelpMessage = 'If specified cookies are loaded into a Microsoft.PowerShell.Commands.WebRequestSession and returned for session use.')] [switch] $WebSession ) Write-Verbose -Message 'Retrieving raw cookies from SQLite database...' try { if ($DomainName) { $cookies = Get-RawCookiesFromDB -Browser $Browser -DomainName $DomainName -ErrorAction 'Stop' } else { $cookies = Get-RawCookiesFromDB -Browser $Browser -ErrorAction 'Stop' } } catch { throw $_ } if ($null -eq $cookies) { Write-Warning -Message 'No cookies were returned from the SQLite database!' return } if ($Browser -eq 'FireFox') { Write-Verbose -Message 'FireFox specified. No cookie decryption is necessary.' if ($WebSession) { Write-Verbose -Message ('Cookie Count: {0}' -f $cookies.Count) if ($cookies.Count -gt 300) { throw 'Only up to 300 cookies can be loaded into a WebSession.' } Write-Verbose -Message 'WebSession specified. Loading cookies into WebSession.' $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession foreach ($cookie in $cookies) { $newCookie = New-Object System.Net.Cookie $newCookie.Name = $cookie.name $newCookie.Value = $cookie.value $newCookie.Domain = $cookie.host try { $session.Cookies.Add($newCookie) } catch { Write-Warning -Message "$($cookie.name) could not be loaded into the session. Skipping." } } #foreach_cookies return $session } #if_websession else { return $cookies } #else_websession } #if_FireFox Write-Verbose -Message 'Getting Cookie decryption key...' if ($IsWindows) { Write-Verbose -Message 'Windows detected...' $cookiesKey = Get-WindowsCookieDecryptKey -Browser $Browser if ($cookiesKey -eq $false) { throw 'Cookie decryption key not retrieved successfully' } #sets the script level decryption key $decryptEval = New-WindowsAesGcm -WinDecrypt $cookiesKey if ($decryptEval -ne $true) { throw 'AesGcm Cookie decryption key not created successfully' } } elseif ($IsLinux) { Write-Verbose -Message 'Linux detected...' #TODO: Add Linux cookie decrypt support Write-Warning -Message 'Linux cookie decryption is not currently supported' return } elseif ($IsMacOS) { Write-Verbose -Message 'MacOS detected...' #TODO: Add MacOS support Write-Warning -Message 'MacOS cookie decryption is not currently supported' return } else { throw 'Unsupported OS. Windows / Linux / OSX' } Write-Verbose -Message 'Decrypting cookies...' foreach ($cookie in $cookies) { $temp = $null $temp = Unprotect-Cookie -Cookie $cookie.encrypted_value $cookie | Add-Member -NotePropertyName decrypted_value -NotePropertyValue $temp } Write-Verbose -Message 'Cookies decryption completed.' if ($WebSession) { Write-Verbose -Message ('Cookie Count: {0}' -f $cookies.Count) if ($cookies.Count -gt 300) { throw 'Only up to 300 cookies can be loaded into a WebSession.' } Write-Verbose -Message 'WebSession specified. Loading cookies into WebSession.' $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession foreach ($cookie in $cookies) { $newCookie = New-Object System.Net.Cookie $newCookie.Name = $cookie.name $newCookie.Value = $cookie.decrypted_value $newCookie.Domain = $cookie.host_key try { $session.Cookies.Add($newCookie) } catch { Write-Warning -Message "$($cookie.name) could not be loaded into the session. Skipping." } } #foreach_cookies return $session } #if_websession else { return $cookies } #else_websession } #Get-DecryptedCookiesInfo <# .EXTERNALHELP ApertaCookie-help.xml #> function Get-RawCookiesFromDB { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Browser choice')] [ValidateSet('Edge', 'Chrome', 'FireFox')] [string] $Browser, [Parameter(Mandatory = $false, HelpMessage = 'Domain to search for in Cookie Database')] [string] $DomainName ) $cookies = $null $osCookieInfo = Get-OSCookieInfo -Browser $Browser if ($null -eq $osCookieInfo) { Write-Warning 'OS Cookie information was not located' return } $sqlPath = $osCookieInfo.SQLitePath $tableName = $osCookieInfo.TableName $copySQLPath = Copy-CookieDBToTemp -SQLitePath $sqlPath Write-Verbose -Message 'Copying sqlite db to temp location for query...' if ($copySQLPath -eq $false) { Write-Warning 'OS Cookie database could not be copied for query!' return } else { Write-Verbose -Message 'Copy completed.' } if ($DomainName) { switch ($Browser) { FireFox { $query = "SELECT `"_rowid_`",* FROM `"main`".`"$tableName`" WHERE `"host`" LIKE '%$DomainName%' ESCAPE '\' LIMIT 0, 49999;" } Default { $query = "SELECT `"_rowid_`",* FROM `"main`".`"$tableName`" WHERE `"host_key`" LIKE '%$DomainName%' ESCAPE '\' LIMIT 0, 49999;" } } } else { $query = "SELECT `"_rowid_`",* FROM `"main`".`"$tableName`" '\' LIMIT 0, 49999;" } Write-Verbose -Message ('Establishing SQLite connection to: {0}' -f $copySQLPath) try { $cookiesSQL = New-SQLiteConnection -DataSource $copySQLPath -ErrorAction 'Stop' } catch { if ($copySQLPath -ne $false) { Write-Verbose -Message 'Attempting sqlite db copy cleanup...' Remove-Item -Path $copySQLPath -Confirm:$false -Force -ErrorAction 'SilentlyContinue' } throw $_ } # examples of things that can be done with the connection: # $cookiesSQL.GetSchema() # $cookiesSQL.GetSchema("Tables") Write-Verbose -Message ('Running query {0} against {1}' -f $query, $sqlPath) try { $dataSource = $cookiesSQL.FileName $sqlSplat = @{ Query = $query DataSource = $dataSource ErrorAction = 'Stop' } $cookies = Invoke-SqliteQuery @sqlSplat } catch { if ($copySQLPath -ne $false) { Write-Verbose -Message 'Attempting sqlite db copy cleanup...' Remove-Item -Path $copySQLPath -Confirm:$false -Force -ErrorAction 'SilentlyContinue' } throw $_ } finally { $cookiesSQL.Close() $cookiesSQL.Dispose() if ($copySQLPath -ne $false) { Write-Verbose -Message 'Attempting sqlite db copy cleanup...' Remove-Item -Path $copySQLPath -Confirm:$false -Force -ErrorAction 'SilentlyContinue' } } Write-Verbose -Message 'Cookies retrieved from SQLite' return $cookies } #Get-RawCookiesFromDB |