OOBE: Story of plaintext password in registry(CVE-2023-21726) 2023/04/26
Summary
A vulnerability [1] (CVE-2023-21726) was found in CredUI (CredPackAuthenticationBufferW [2]) Windows API function that lead to a plaintext password leakage. While we will only discuss one example of possible attack, in theory any application that used Windows API CredPackAuthenticationBufferW (CRED_PACK_PROTECTED_CREDENTIALS, ...) could have saved plaintext password somewhere on the system.
Besides CredUI, we also would like to mention strange behavior of OOBE (Out-of-Box Experience [3]), that is trying to cache credentials of a newly created user account in registry (accessible by unprivileged Medium integrity user). This is what lead us to finding the problem in the first place.
Finding the password
It is common to applications to store their configuration data on a disk. Usually, it will be stored in some encrypted form. Algorithm may vary from DPAPI’s CryptProtectData data blob to some self-made encryption.
But occasionally, because of bad security model design or developer’s mistakes, the important data may end up being stored in a plaintext format.
That’s why, sometimes it’s tempting to scan raw storage/memory for some interesting strings including plaintext passwords. In most cases you will get false positive hits inside random junk of data, but once in a while you will also find something unexpected.
So, what we did, is just installed a fresh Windows and created first password protected account as was offered to us by OOBE. After scanning Virtual Machine’s disk drive, we found our newly created password as a plaintext in blocks allocated for following file: C:\Windows\system32\config\DEFAULT. And the corresponding registry key is (readable by unprivileged users):
HKEY_USERS\.DEFAULT\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker\LocalSystemAuthBuffer
Technical details (CredUI side)
Main problem was located in credui.dll
. Despite user supplying CRED_PACK_PROTECTED_CREDENTIALS flag, no encryption was done for password whatsoever.
We can create a simple program to demonstrate this problem:
#include <windows.h>
#include <stdio.h>
#include <wincred.h>
#pragma comment(lib,"Credui.lib")
void HexTable(pCred, dwCredSize) {
unsigned char *ptr = (unsigned char *)pCred;
for (size_t i = 0; i < dwCredSize; i++) {
if (i % 16 == 0) {
printf("%08zx: ", i);
}
printf("%02x ", ptr[i]);
if (i % 16 == 15) {
printf(" |");
for (size_t j = i - 15; j <= i; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
printf("|\n");
}
}
}
int main() {
DWORD dwCredSize = 0x1000;
PBYTE pCred = (PBYTE)malloc(0x1000);
if (CredPackAuthenticationBufferW(CRED_PACK_PROTECTED_CREDENTIALS, (LPWSTR) L"UserName", (LPWSTR)L"P@ssw0rd123", pCred, &dwCredSize)) {
HexTable(pCred, dwCredSize);
} else {
printf("CredUnPackAuthenticationBufferW LastError=%08x", GetLastError());
}
}
Let’s run it before and after a fix:
As we can see password is now seems encrypted. And it is actually done by internal call to analog of CryptProtectMemory (CRYPTPROTECTMEMORY_SAME_LOGON) [4], which prevents users with LUID not equal to the one which used during encryption from decrypting the password. And even more than that, after reboot password is becoming impossible to decrypt, not even in the same security context (because global key values used for encryption are randomly generated by cng.sys
on computer start).
Current call flow:
CredPackAuthenticationBufferW (cryptui.dll) | - CredPackKerbBufferFromStrings (cryptui.dll) | -- CredProtectEx (sechost.dll) | --- CredpEncryptAndMarshalBinaryBlobEx (sechost.dll) | ---- CredpEncodeSecretEx (sechost.dll) | ------ SystemFunction040 / RtlEncryptMemory(RTL_ENCRYPT_OPTION_SAME_LOGON) (cryptbase.dll) | ------- NtDeviceIoControlFile ("\\??\KSecDD", 0x39001E, ...) | .... | -------- CngEncryptMemoryEx (cng.sys)
You can find a very detailed description on how CngEncryptMemoryEx works in [5] (flare-on, KeePass writeup).
We can also highlight the place in cryptui.dll
which received the fix:
After fix:
So now password of the first local administrator can be considered securely encrypted for use during first computer start.
Technical details (OOBE side)
Here is an example of what we call UserOOBE (Windows 11):
This screen appears right after Windows installation and allows user to set up account as well as apply some other configurations.
It is hosted in explorer.exe process and implemented by UserOOBE.dll
library. Everything is done under defaultuser0 account.
Notice: We are only looking in now non default installation flow (as of Windows 11) where user creates local account and rejects offer to use Microsoft Account (oobe\\bypassnro).
After user inputs password and security questions, a WinRT call is made to CloudExperienceHostBroker::Account::LocalAccountManager::CreateLocalAccountWithRecoveryKindAsync in CloudExperienceHostBroker.dll
hosted by process RuntimeBroker.exe.
This function creates a new administrative account with specified details and also trying to cache the plaintext password for non obvious reason (we may assume it is somehow related to OneDrive account registration as one of related interfaces is called IOOBEOneDriveOptin).
Caching is done via calls to in-proc COM server implemented in msoobeplugins.dll
.
PackAuthBuffer is effectively just doing a call to CredPackAuthentificationBufferW with CRED_PACK_PROTECTED_CREDENTIALS. At the time of report, this function did not encrypt provided credentials, only converted them to serialized format.
Finally, resulting buffer saved into registry by calling CacheAuthBuffer.
HKEY_USERS\.DEFAULT\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker\LocalSystemAuthBuffer
At the end we were unable to locate any usage of this stored password.
Official mitigation
We already showed a general CredUI fix which will prevent future leakage of credentials. Still the question remains on what will be with the stations that already have saved plaintext password in the registry key.
Here is where new OOBE-Maintenance.exe binary comes into play.
It seems that security update packages from January 2023 create a scheduled task \Microsoft\Windows\Registry\OOBE-Maintenance which will execute this binary once.
The logic of binary is simple:
1. Remove any cached passwords in LocalSystemAuthBuffer
2. Save DeviceMigitationStatus 1
3. Remove scheduled task OOBE-Maintenance
For a fresh patched installations password is still stored in that registry key, but is now encrypted and from the start accompanied by value DeviceMigitationStatus=1.
Testing and exploitation
・To get the value in HEX string:
[string]::join(' ',((Get-Item -Path Registry::HKEY_USERS\.Default\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker).GetValue('LocalSystemAuthBuffer')| ForE ach{'{0:x2}' -f $_}))
・To get the value in ASCII string:
[System.Text.Encoding]::ASCII.GetString((Get-Item -Path Registry::HKEY_USERS\.Default\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker).GetValue('LocalSystemAuthBuffer'))
Timeline
• 2021/12/24 - Report create on MSRC
• 2022/03/23 - MSRC informed us that the fix is expected after 2023/01/11
• 2023/01/23 - Fix released with CVE-2023-21726
Links
[1] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21726
[2] https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credpackauthenticationbufferw
[3] https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/customize-oobe-in-windows-11
[4] https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectmemory
[5] https://github.com/eleemosynator/writeups/blob/master/flare-on-6/12%20-%20help/readme.md#7-the-shortening-of-the-way