Disabling the Scripting.FileSystemObject ComObject (When you get the 0x8002801c error)


While working with a customer to automate the hardening process (STIG: Security Technical Implementation Guide) for IIS servers, we ran into a problem (error 0x8002801c) when we tried to run the following command in order to disable (unregister) the Scripting.FileSystemObject ComObject

C:\Windows\System32\regsvr32.exe /u scrrun.dll

The 0x8002801c error translates to TYPE_E_REGISTRYACCESS. Using Sysinternal’s Process Monitor (aka procmon), we found that it was failing because of missing permissions to the following registry keys:

HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}\1.0\0\win32
HKEY_CLASSES_ROOT\Scripting.FileSystemObject\CLSID

Note that adding permissions to the first key only (as suggested in Finding V-13700 @ stigviewer.com) is not enough. The command would run without errors, the validation would pass (thinking that FSO is disabled) but you could still create and use FSO objects.

So for automating the whole process, we needed to take ownership on the registry keys, add permissions to the keys, and then run the command to unregister the DLL.

This results in the following code:

#region Helper functions
function Set-Ownership {
[CmdletBinding(SupportsShouldProcess = $false)]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()] [ValidatePattern('(\w+)\\(\w+)')]
[string]$Identity,
[Parameter(Mandatory = $false)]
[switch]$AddFullControl,
[Parameter(Mandatory = $false)]
[switch]$Recurse
)
begin {
$tokenManipulate = @'
using System;
using System.Runtime.InteropServices;
public class TokenManipulate {
[DllImport("kernel32.dll", ExactSpelling = true)]
internal static extern IntPtr GetCurrentProcess();
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
[DllImport("advapi32.dll", SetLastError = true)]
internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct TokPriv1Luid {
public int Count;
public long Luid;
public int Attr;
}
internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
internal const int TOKEN_QUERY = 0x00000008;
internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
public static bool AddPrivilege(string privilege) {
bool retVal;
TokPriv1Luid tp;
IntPtr hproc = GetCurrentProcess();
IntPtr htok = IntPtr.Zero;
retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
tp.Count = 1;
tp.Luid = 0;
tp.Attr = SE_PRIVILEGE_ENABLED;
retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
return retVal;
}
public static bool RemovePrivilege(string privilege) {
bool retVal;
TokPriv1Luid tp;
IntPtr hproc = GetCurrentProcess();
IntPtr htok = IntPtr.Zero;
retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
tp.Count = 1;
tp.Luid = 0;
tp.Attr = SE_PRIVILEGE_DISABLED;
retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
return retVal;
}
}
'@
}
Process {
Add-Type -TypeDefinition $TokenManipulate
[void][TokenManipulate]::AddPrivilege('SeTakeOwnershipPrivilege')
[void][TokenManipulate]::AddPrivilege('SeRestorePrivilege')
$item = Get-Item -Path $Path -ErrorAction SilentlyContinue
if(-not $item) {
Write-Warning ("'{0}' not found" -f $Path)
return
}
$owner = New-Object System.Security.Principal.NTAccount -ArgumentList ($Identity -split '\\')
if ($item.PSIsContainer) {
switch ($item.PSProvider.Name) {
'FileSystem' {
$acl = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
}
'Registry' {
$acl = New-Object -TypeName System.Security.AccessControl.RegistrySecurity
switch (($item.Name -split '\\')[0]) {
'HKEY_CLASSES_ROOT' { $rootKey = [Microsoft.Win32.Registry]::ClassesRoot; break }
'HKEY_LOCAL_MACHINE' { $rootKey = [Microsoft.Win32.Registry]::LocalMachine; break }
'HKEY_CURRENT_USER' { $rootKey = [Microsoft.Win32.Registry]::CurrentUser; break }
'HKEY_USERS' { $rootKey = [Microsoft.Win32.Registry]::Users; break }
'HKEY_CURRENT_CONFIG' { $rootKey = [Microsoft.Win32.Registry]::CurrentConfig; break }
}
$key = $item.Name -replace "$rootKey\\"
$item = $rootKey.OpenSubKey($Key, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
[System.Security.AccessControl.RegistryRights]::TakeOwnership)
}
}
Write-Verbose "Setting ownership for $($owner.Value) on $Path"
$acl.SetOwner($owner)
$item.SetAccessControl($acl)
if($AddFullControl) {
$ace = New-Object -TypeName System.Security.AccessControl.RegistryAccessRule -ArgumentList @(
$owner, [System.Security.AccessControl.RegistryRights]::FullControl,
[System.Security.AccessControl.InheritanceFlags]::None,
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow
)
Write-Verbose "Setting FullControl permissions for $($owner.Value) on $Path"
$acl.AddAccessRule($ace)
$item.SetAccessControl($acl)
}
if ($item.PSProvider.Name -eq 'Registry') { $item.Close() }
if ($Recurse.IsPresent) {
if ($item.PSProvider.Name -eq 'Registry') {
$items = @(Get-ChildItem -Path $Path -Recurse -Force | Where-Object { $_.PSIsContainer })
}
else {
$items = @(Get-ChildItem -Path $Path -Recurse -Force)
}
for ($i = 0; $i -lt $items.Count; $i++) {
switch ($item.PSProvider.Name) {
'FileSystem' {
$item = Get-Item $items[$i].FullName
if ($item.PSIsContainer) { $acl = New-Object -TypeName System.Security.AccessControl.DirectorySecurity }
else { $acl = New-Object -TypeName System.Security.AccessControl.FileSecurity }
}
'Registry' {
$item = Get-Item $items[$i].PSPath
$acl = New-Object -TypeName System.Security.AccessControl.RegistrySecurity
switch ($item.Name.Split('\')[0]) {
'HKEY_CLASSES_ROOT' { $rootKey = [Microsoft.Win32.Registry]::ClassesRoot; break }
'HKEY_LOCAL_MACHINE' { $rootKey = [Microsoft.Win32.Registry]::LocalMachine; break }
'HKEY_CURRENT_USER' { $rootKey = [Microsoft.Win32.Registry]::CurrentUser; break }
'HKEY_USERS' { $rootKey = [Microsoft.Win32.Registry]::Users; break }
'HKEY_CURRENT_CONFIG' { $rootKey = [Microsoft.Win32.Registry]::CurrentConfig; break }
}
$Key = $item.Name.Replace(($item.Name.Split('\')[0] + '\'), '')
$item = $rootKey.OpenSubKey($Key, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
[System.Security.AccessControl.RegistryRights]::TakeOwnership)
}
}
$acl.SetOwner($owner)
Write-Verbose "Setting ownership for $($owner.Value) on $($item.Name)"
$item.SetAccessControl($acl)
if ($item.PSProvider.Name -eq 'Registry') { $item.Close() }
}
}
}
else {
if ($Recurse.IsPresent) { Write-Warning 'Object specified is neither a folder nor a registry key. Recursion is not possible.' }
switch ($item.PSProvider.Name) {
'FileSystem' { $acl = New-Object -TypeName System.Security.AccessControl.FileSecurity }
'Registry' { Write-Error 'You cannot set ownership on a registry value' }
default { Write-Error "Unknown provider: $($item.PSProvider.Name)" }
}
$acl.SetOwner($owner)
Write-Verbose "Setting ownership for $($owner.Value) on $Path"
$item.SetAccessControl($acl)
}
}
}
#endregion
#region Test FileSystemObject ComObject (Should work)
(New-Object -ComObject Scripting.FileSystemObject).Drives
#endregion
#region Set ownership and add permissions to the relevant registry keys
$regPaths = @(
'Registry::HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}\1.0\0\win32'
'Registry::HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}\1.0\0\win64'
'Registry::HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}\1.0\0'
'Registry::HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}\1.0\FLAGS'
'Registry::HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}\1.0\HELPDIR'
'Registry::HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}\1.0'
'Registry::HKEY_CLASSES_ROOT\TypeLib\{420B2830-E718-11CF-893D-00A0C9054228}'
)
foreach($regPath in $regPaths) {
Set-Ownership -Path $regPath -Identity 'BUILTIN\Administrators' -AddFullControl -Recurse -Verbose
}
#endregion
#region unregister the FileSystemObject scrrun dll
C:\Windows\System32\regsvr32.exe scrrun.dll /u /s
#endregion
#region Test FileSystemObject ComObject (Should fail)
(New-Object -ComObject Scripting.FileSystemObject).Drives
#endregion

HTH,
Martin.

Add permissions to a Session Configuration


Though the recommended approach would be to upgrade to PowerShell 5.1 and implement JEA (preferable with DSC and the JEA DSC module), there sometimes might be a need to programmatically add permissions to a PowerShell session configuration.

Continuing the mentioned above, and a question asked on the reddit forum, below is an example on how to add a specific permission (ACE) to the configuration session permissions (ACL):

# The identity to add permissions for
$Identity = "myDomain\nonAdmins"
# The configuration name to change permissions to (default is 'microsoft.powershell')
$sessionConfigurationName = 'microsoft.powershell'
# Get the current permissions on the default endpoint
$sddl = (Get-PSSessionConfiguration -Name $sessionConfigurationName).SecurityDescriptorSddl
# Build the new Access Control Entry object
$rights = -1610612736 # AccessAllowed
$IdentitySID = ((New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $Identity).Translate(
[System.Security.Principal.SecurityIdentifier])).Value
$newAce = New-Object System.Security.AccessControl.CommonAce(
[System.Security.AccessControl.AceFlags]::None,
[System.Security.AccessControl.AceQualifier]::AccessAllowed,
$rights, $IdentitySID, $false, $null
)
# Prepare the RawSecurityDescriptor
$rawSD = New-Object -TypeName System.Security.AccessControl.RawSecurityDescriptor -ArgumentList $sddl
if ($rawSD.DiscretionaryAcl.GetEnumerator() -notcontains $newAce) {
$rawSD.DiscretionaryAcl.InsertAce($rawSD.DiscretionaryAcl.Count, $newAce)
}
$newSDDL = $rawSD.GetSddlForm([System.Security.AccessControl.AccessControlSections]::All)
# Set the PSSessionConfiguration permissions
Set-PSSessionConfiguration -Name $sessionConfigurationName -SecurityDescriptorSddl $newSDDL
# Verify permissions were added
(Get-PSSessionConfiguration -Name $sessionConfigurationName).Permission -split ', '

HTH,
Martin.

Embed PowerShell code in a batch file


In a certain scenario, I needed a batch file (bat or cmd extension) that runs PowerShell code, and I could have only one file, so I couldn’t go with the easy way of a batch file calling PowerShell.exe with the -File parameter specifying the path to a ps1 file.

For this, I created a special batch file with a header that reads the contents of itself, excludes the lines that have the batch code (lines stat start with @@) and then runs the rest in PowerShell.

Here is the batch template, just replace the lines below the comment that says “POWERSHELL CODE STARTS HERE” with your PowerShell code.

@ECHO off
@setlocal EnableDelayedExpansion
@goto label1
@echo this lines should not be printed
@echo and this whole block (lines 4-8) can actually be removed
@echo I added these just for Charlie22911
@:label1
@set LF=^
@SET command=#
@FOR /F "tokens=*" %%i in ('findstr -bv @ "%~f0"') DO SET command=!command!!LF!%%i
@powershell -noprofile -noexit -command !command! & goto:eof
# *** POWERSHELL CODE STARTS HERE *** #
Write-Host 'This is PowerShell code being run from inside a batch file!' -Fore red
$PSVersionTable
Get-Process -Id $PID | Format-Table

Though not intended, it’s another way of bypassing the ExecutionPolicy even if it’s set in Group Policy, since the code is run as commands and not a script file.

HTH,
Martin.

Get the certificate selected in Get-Credential


Following Matt Bongiovi’s post at the Hey, Scripting Guy! Blog about PowerShell support for certificate credentials, I ported the main parts of the c# code he references in his post to PowerShell.

So here you have, a quick-and-dirty Get-CertificateFromCredential function you can use to get the certificate for the credentials the user selected from the drop down in the Get-Credential window:

function Get-CertificateFromCredential {
param([PSCredential]$Credential)
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class NativeMethods {
public enum CRED_MARSHAL_TYPE {
CertCredential = 1,
UsernameTargetCredential
}
[StructLayout(LayoutKind.Sequential)]
public struct CERT_CREDENTIAL_INFO {
public uint cbSize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] rgbHashOfCert;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredUnmarshalCredential(
IntPtr MarshaledCredential,
out CRED_MARSHAL_TYPE CredType,
out IntPtr Credential
);
}
'@ -ReferencedAssemblies System.Runtime.InteropServices
$credData = [IntPtr]::Zero
$credInfo = [IntPtr]::Zero
$credType = [NativeMethods+CRED_MARSHAL_TYPE]::CertCredential
try {
$credData = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni($Credential.UserName);
$success = [NativeMethods]::CredUnmarshalCredential($credData, [ref] $credType, [ref] $credInfo)
if ($success) {
[NativeMethods+CERT_CREDENTIAL_INFO] $certStruct = [NativeMethods+CERT_CREDENTIAL_INFO][System.Runtime.InteropServices.Marshal]::PtrToStructure(
$credInfo, [System.Type][NativeMethods+CERT_CREDENTIAL_INFO])
[byte[]] $rgbHash = $certStruct.rgbHashOfCert
[string] $hex = [BitConverter]::ToString($rgbHash) -replace '-'
$certCredential = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList @(
[System.Security.Cryptography.X509Certificates.StoreName]::My,
[System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser
)
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
$certsReturned = $store.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $hex, $false)
if($null -eq $certsReturned) {
throw ('Could not find a certificate with thumbprint {0}' -f $hex)
}
$certsReturned[0]
}
} catch {
throw ('An error occured: {0}' -f $_.Exception.Message)
}
finally {
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($credData)
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($credInfo)
if($null -ne $store) { $store.Close() }
}
}

Then, you can use the function:


$cred = Get-Credential -Message 'Select the SMARTCARD'
Get-CertificateFromCredential -Credential $cred

Important: Keep in mind that Get-Credential cmdlet doesn’t verify the credentials anywhere, it just opens the $Host.UI.PromptForCredential popup and returns a PSCredential object. The credentials themselves are verified only when used with another cmdlet.
This means that the user can select a certificate from the dropdown in the Get-Credential window, enter an incorrect PIN and this function will still return the certificate.

I’ve also been following issue #3048 in the PowerShell repository on github. Hopefully, native support for certificate authentication will be added in a future version (6.1.0?)

HTH,
Martin.

Install and Configure a Group Managed Service Account with PowerShell


Managed Service Accounts (MSAs) were introduced in Windows Server 2008, and Group Managed Service Accounts (gMSAs) were introduced in Windows Server 2012.
Since then, a lot has been said about gMSAs (see the references section at the bottom).
So in this post, I’ll just summarize the flow and the PowerShell commands needed for each step in the process of creating and using gMSAs.

Before we really start, let’s set some variables we’ll use later on in our commands:


$gMSA_Name = 'gmsaWWW'
$gMSA_FQDN = 'gmsaWWW.contoso.com'
$gMSA_SPNs = 'http/www', 'http/www.contoso.com'
$gMSA_HostNames = 'webServer01', 'webServer02', 'webServer03'

Step #1: Create the KDS Root Key (required only once per domain). This is used by the KDS service on DCs to generate passwords:


Add-KDSRootKey -EffectiveTime (Get-Date).AddHours(-10)

This is a workaround to the safety measure that makes us wait 10 hours (by default), to make sure all DCs have replicated and are able to respond to gMSA requests

Step #2: Get the principals (computer accounts) of the hosts that will use the gMSA:


$gMSA_HostsGroup = $gMSA_HostNames | ForEach-Object { Get-ADComputer -Identity $_ }

Or Create a group, and add the computer accounts to it:


$gMSA_HostsGroupName = "$($gMSA_Name)_HostsGroup"
$gMSA_HostsGroup = New-ADGroup -Name $gMSA_HostsGroupName -GroupScope Global -PassThru
$gMSA_HostNames | ForEach-Object { Get-ADComputer -Identity $_ } | 
    ForEach-Object { Add-ADGroupMember -Identity $gMSA_HostsGroupName -Members $_ }

Step #3: Create and Configure the gMSA


New-ADServiceAccount -Name $gMSA_Name -DNSHostName $gMSA_FQDN -PrincipalsAllowedToRetrieveManagedPassword $gMSA_HostsGroup -ServicePrincipalNames $gMSA_SPNs

Set #4 (optional): If delegation is required:
Enable ‘Trust this user for delegation to any service’ (a.k.a. “Unconstrained Delegation”)


Set-ADServiceAccount -Identity $gMSA_Name -TrustedForDelegation $true

Note: To enable “Constrained Delegation“, you need to set it at creation time, using the -OtherAttributes parameter:


$myBackendSPNs = 'http/webBackend', 'http/webBackend.contoso.com'
New-ADServiceAccount -Name $gMSA_Name -DNSHostName $gMSA_FQDN -PrincipalsAllowedToRetrieveManagedPassword $gMSA_HostsGroup -ServicePrincipalNames $gMSA_SPNs -OtherAttributes @{'msDS-AllowedToDelegateTo'=$myBackendSPNs}

Step #5: Then, on the Member servers that needs to use the gMSA:


$gMSA_Name = 'gmsaWWW'

(Optional) Install the gMSA:


Add-WindowsFeature -Name RSAT-AD-PowerShell 
Install-ADServiceAccount -Identity $gMSA_Name

This verifies that the computer is eligible to host the gMSA, retrieves the credentials and stores the account information on the local computer (using the NetAddServiceAccount function)

(Optional) Test the gMSA:


Test-ADServiceAccount -Identity $gMSA_Name

(Optional) Set the gMSA on an IIS ApplicationPool:


Import-Module WebAdministration
$pool = Get-Item IIS:AppPoolsDefaultAppPool
$pool.processModel.identityType = 3
$pool.processModel.userName = "$env:USERDOMAIN$gMSA_Name$"
$pool.processModel.password = ''
$pool | Set-Item

(Optional) Set the gMSA on a Windows Service:


$serviceName = 'myService'
$service = Get-WmiObject -Class Win32_Service -Filter "Name='$serviceName'"
$service.Change($null, $null, $null, $null, $null, $null, 
    "$env:USERDOMAIN$gMSA_Name$", $null, $null, $null, $null)

Note: If you use the UI (services.msc or IIS Manager) to set the LogOn credentials for the service or ApplicationPool Identity, don’t forget to add the $ (dollar sign) at the end of the gMSA name. As it represents the service account object, that inherits from the computer account object.

References:

HTH,
Martin.