Jump to content
MetaMSP

There and Back Again, or: Working with HKEY_USERS with PowerShell in Automate

Recommended Posts

Posted (edited)

It seems to be that deploying something to all HKEY_USERS is something that there is a lot of hubbub about but nothing directly lift-and-shiftable into Automate, except for DarrenWhite99's glorious REG-MULTI batch (!!), which can be found at https://www.mspgeek.com/topic/3528-how-would-i-run-a-script-once-user-logs-into-a-computer/?tab=comments#comment-21905 -- There has to be a better way to do this through something modern, like PowerShell, right? Nope. See sentence 1 - everything Google has found for me has been lacking, so here we go...

The inherent challenge is this:

  • Automate Agent runs as LOCAL SYSTEM, thus in the context of user SID S-1-5-18.
  • Any commands run through Automate (that are not otherwise specified) run under the context of the Automate Agent, thus limiting the usefulness of any bare user profile command or applet.
  • Using Group Policy Preferences (ref: https://www.grouppolicy.biz/2010/03/what-are-group-policy-preferences/) assumes the existence of Group Policy, thus assuming that an endpoint is a domain member, which is not applicable in all use cases nor is easily manageable via Automate.
  • Some user profiles may never interactively log in to a given device, thus limiting the usefulness of Microsoft Active Setup (ref: https://en.wikipedia.org/wiki/Active_Setup).

Let's start with dumping the subkeys from HKEY_USERS. PowerShell has a registry key provider (ref: https://docs.microsoft.com/en-us/powershell/module/Microsoft.PowerShell.Core/About/about_Registry_Provider)... surely we can use that to enumerate HKEY_USERS, right?
image.png.a354935bcb2eae5746a8e3886a9edb65.png

So, the built-in registry key provider is only aware of HKEY_LOCAL_MACHINE and HKEY_CURRENT_USER... you can't get there from here. However, PoSh allows us to extend its functionality by directly referencing .Net classes. Let me repeat that: we can reference and use native .Net Framework classes directly in PowerShell. https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators calls this the static member operator:image.png.fb37aef3a09fcdae72f7a9f96be5ddd6.png

Well, now that's interesting, isn't it? Google-fu yields https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.registrykey as the .Net encapsulation of a registry key. Looking through the methods, we find the RegistryKey.OpenRemoteBaseKey(RegistryHive, String) method. All we need to do is pass it the desired hive (https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.registryhive) and the target computer name... let's see what happens!

$targetComputer = $env:COMPUTERNAME;
$availableProfiles = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::Users, $targetComputer).GetSubkeyNames();
$availableProfiles;

*** Sanitized Output ***
.DEFAULT
S-1-5-19
S-1-5-20
S-1-5-21-ddddddddd-dddddddddd-dddddddddd-dddd
S-1-5-21-ddddddddd-dddddddddd-dddddddddd-dddd_Classes
S-1-5-18

Hey, now we're getting somewhere! That output looks an awful lot like RegEdit:
image.png.3fff60989026a40e3e9a8befe5f0a867.png

Now let's add the ability to target certain subsets of HKU based off of the well-known SIDs (https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems) themselves, and also remove the _Classes subkeys from our target list (ref: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_variables) (ref: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_foreach) (ref: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_switch😞

[string]$targetComputer = $env:COMPUTERNAME;
[bool]$targetDefaultUser = $true;
[bool]$targetLocalSystem = $true;
[bool]$targetLocalService = $true;
[bool]$targetNetworkService = $true;
[bool]$targetOtherSIDs = $true;

[array]$profilesAvailable = $null;
[array]$profilesTargeted = $null;

$profilesAvailable = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::Users, $targetComputer).GetSubkeyNames();
foreach ($profile in $profilesAvailable) {
  switch ($profile) {
    '.DEFAULT' { if ($targetDefaultUser)  n { $profilesTargeted += $profile; }; break; };
    'S-1-5-18' { if ($targetLocalSystem)    { $profilesTargeted += $profile; }; break; };
    'S-1-5-19' { if ($targetLocalService)   { $profilesTargeted += $profile; }; break; };
    'S-1-5-20' { if ($targetNetworkService) { $profilesTargeted += $profile; }; break; };
    default    {
      if ($targetOtherSIDs) { try {
        $candidateSID = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $profile;
        if ($candidateSID.IsAccountSid()) { $profilesTargeted += $profile; };
        } catch {};
      }
    }
  }
}
$profilesTargeted;

*** Sanitized Output ***
.DEFAULT
S-1-5-19
S-1-5-20
S-1-5-21-ddddddddd-dddddddddd-dddddddddd-1001
S-1-5-18

Fantastic - we now have a list of HKU subkeys for processing! We're done, right?
...
...
...
Of course not! Now we need to actually do something with those key paths. Let's change our concatenation statements to include the HKEY_USERS hive prefix, change a couple variable names for self-documentation's sake, and see how we can use the generated path to call out to a heavy lifter:

[string]$targetComputer     = $env:COMPUTERNAME;
[string]$targetSubkey       = '\Control Panel\International';
[bool]$targetDefaultUser    = $true;
[bool]$targetLocalSystem    = $true;
[bool]$targetLocalService   = $true;
[bool]$targetNetworkService = $true;
[bool]$targetOtherSIDs      = $true;

[array]$profilesAvailable   = $null;
[string]$candidateProfile   = $null;
[array]$HKUsTargeted        = $null;
[string]$enumeratedHKUPath  = $null;

$profilesAvailable = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::Users, $targetComputer).GetSubkeyNames();
foreach ($candidateProfile in $profilesAvailable) {
  switch ($candidateProfile) {
    '.DEFAULT' { if ($targetDefaultUser)    { $HKUsTargeted += 'HKEY_USERS\' + $candidateProfile; }; break; };
    'S-1-5-18' { if ($targetLocalSystem)    { $HKUsTargeted += 'HKEY_USERS\' + $candidateProfile; }; break; };
    'S-1-5-19' { if ($targetLocalService)   { $HKUsTargeted += 'HKEY_USERS\' + $candidateProfile; }; break; };
    'S-1-5-20' { if ($targetNetworkService) { $HKUsTargeted += 'HKEY_USERS\' + $candidateProfile; }; break; };
    default    {
      if ($targetOtherSIDs) { try {
        $candidateSID = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $candidateProfile;
        if ($candidateSID.IsAccountSid()) { $HKUsTargeted += 'HKEY_USERS\' + $candidateProfile; };
        } catch {};
      }
    }
  }
}
foreach ($enumeratedHKUPath in $HKUsTargeted) {
  REG QUERY ($enumeratedHKUPath+$targetSubkey) /v "LocaleName" | ? {$_.trim() -ne "" } 
}

*** Sanitized Output ***
HKEY_USERS\.DEFAULT\Control Panel\International
    LocaleName    REG_SZ    en-US
HKEY_USERS\S-1-5-19\Control Panel\International
    LocaleName    REG_SZ    en-US
HKEY_USERS\S-1-5-20\Control Panel\International
    LocaleName    REG_SZ    en-US
HKEY_USERS\S-1-5-21-ddddddddd-dddddddddd-dddddddddd-1001\Control Panel\International
    LocaleName    REG_SZ    en-US
HKEY_USERS\S-1-5-18\Control Panel\International
    LocaleName    REG_SZ    en-US

Hopefully this saves others some time; I know it will save me some to have a template to refer back to each time I need to do something for all HKEY_CURRENT_USERS on a machine again :)

Regards,

Ryan

Edited by MetaMSP
  • Thanks 1

Share this post


Link to post
Share on other sites

Here is a simpler version of the same thing that executes under cmd.exe at a fraction of the time of the PowerShell script with much less system resources and identical output:

for /f "tokens=*" %a in ('reg query "HKU" ^| find /I /V "Classes"') do @(reg query "%~a\Control Panel\International" /v "LocaleName" | findstr /I "International LocaleName")

 

  • Like 1
  • Thanks 1

Share this post


Link to post
Share on other sites

For later reference, here's another approach, modified from https://www.pdq.com/blog/modifying-the-registry-users-powershell/

$RegKey = 'Software\Microsoft\Office\16.0\Common\Identity'
$RegValue ='EnableADAL'
$PatternSID = 'S-1-5-21-\d+-\d+\-\d+\-\d+$'
#####################################################################
$ProfileList = gp 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*' | Where-Object {$_.PSChildName -match $PatternSID} | 
    Select  @{name="SID";expression={$_.PSChildName}}, 
            @{name="UserHive";expression={"$($_.ProfileImagePath)\ntuser.dat"}}, 
            @{name="Username";expression={$_.ProfileImagePath -replace '^(.*[\\\/])', ''}}
$LoadedHives = gci Registry::HKEY_USERS | ? {$_.PSChildname -match $PatternSID} | Select @{name="SID";expression={$_.PSChildName}}
$UnloadedHives = Compare-Object $ProfileList.SID $LoadedHives.SID | Select @{name="SID";expression={$_.InputObject}}, UserHive, Username
Foreach ($item in $ProfileList) {
    If ($item.SID -in $UnloadedHives.SID) {
        "Hive for SID " + $item.SID + " loading..."
        reg load HKU\$($Item.SID) $($Item.UserHive) | Out-Null
    } Else { "Hive for SID " + $item.SID + " already loaded!" }
    #####################################################################
    "User: {0} `r`n" -f $($item.Username) | Write-Host
    If (Test-Path -Path registry::HKEY_USERS\$($Item.SID)\$($RegKey)) {
        Get-ItemProperty -Path registry::HKEY_USERS\$($Item.SID)\$($RegKey) | 
            Foreach {"{0} {1}: {2} {3}" -f "    ", $RegValue, $($_.$RegValue), "`r`n`r`n" | Write-Host}
    } Else { "Registry key not found!" }    
    #####################################################################
    IF ($item.SID -in $UnloadedHives.SID) {
        [gc]::Collect()
        reg unload HKU\$($Item.SID) | Out-Null
    }
}

 

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...