Security Compliance checking with PowerShell: roles, role-combinations and other basic checks

In my last post, I already pointed out 2 security considerations that apply across roles: which combination of server roles is allowed and is a specific role suited (and preferably installed on) a Windows Server CORE?

Besides these 2, there are a number of other things you might want to check if you want to see if a W2K8R2 server is configured securely. So, a more complete list of checks (summarized from the Windows 2008 R2 Security guide) would look as follows:

  • Which server-roles allowed on your networks? Which features do you want to allow?

Some organizations might not want to allow specific server roles on their networks. For example Certificate Server: this is normally a role that you only install on a limited number of highly secured servers. Or maybe you might only want to allow a server role if you have security guidance and checklists available for them? The same might be true for server features, such as the "Wireless Lan Service" or "Telnet Server".

  • What role combinations are allowed?

A general recommendation from the security guides is to limit role-combinations. Combining roles will increase the attack surface of a server. Further, it will also complicate the management of servers, especially if role A is managed by another team than role B. Some other roles on the other hand are often combined, such as application-server roles or Domain Controller and DNS.

  • Should the server be installed on Windows Server CORE?

Windows Server CORE significantly reduces the attack surface of a server, since all UI-related binaries are not installed: no Internet Explorer vulnerabilities, no GDI vulnerabilities, etc. Therefore, you might want to have your most critical servers installed on Windows Server CORE, such as your Domain Controllers, Hyper-V servers, but maybe also the infrastructure servers (DNS, DHCP, File, Print, etc).

  • Is the computer joined to a domain?

Joining a computer to a domain brings a large number of immediate security benefits: users are managed on a central infrastructure (AD), you automatically use a strong network authentication protocol (Kerberos), you can control the security configuration of your servers centrally (apply security baseline GPOs), etc.

  • Has the security baseline GPO been applied?

You do want to check quickly if you haven't forgotten to move the server to the right OU to ensure that all security baseline GPOs are applied correctly to the server. There are multiple ways to do this (e.g. you could use SCCM/DCM functionality to validate each setting), but a quick check might be to just check if a policy names xyz has been applied. Or if the legal notice has been set (the legal notice is not set by default and is typically unique for an organization, so easily identifiable).

  • Are security updates installed on a regular basis?

While you're checking if a server is secure, you might as well quickly want to check if its WSUS configuration (or SCCM) is correct or at least, that the server has received security updates recently. A quick check ("when has the latest patch been applied?") is in no way a replacement for a full check of course (using MBSA or equivalent tools). But at least, you know that the server is receiving security updates.

  • How many users are in the "administrator" role on the server?

A last thing you might want to check is the number of users that have administrative rights on the server; normally, this should only be a small group and membership of the local "administrators" group on a server should be limited to only those users that really need it (i.e. server administrators)

Does all of this sound obvious?

Well, it is actually. But it wouldn't come to a surprise that in any organization, you will find servers that are not configured correctly and/or securely. Even IT-Pro's do make a mistake every now and then :-).

So, how to check all of this using PowerShell?

Let's start with allowed server roles, server-role combinations and features.

Windows Server 2008 R2 has a great build-in module to support all this: ServerManager.

Following code allows you to retrieve all installed roles on server

Import-module ServerManager

$installedRoles = Get-WindowsFeature | where {$_.featureType -eq "Role" -and $_.Installed -eq $true} | select name, DisplayName

The FeatureType property could have any of the following values:

  • Role
  • Role Service
  • Feature

To compare with your list of approved roles (or role-combinations), you could work with an array or hash-list that contains all allowed roles, and for each role, the allowed role-combinations. The sample script (at the end of this blog) shows how to implement this in PowerShell for a case where you would allow only 5 roles and specific role-combinations (IMPORTANT NOTE: the script does not give any security recommendations for role-combinations or allowed roles; it's just a sample how you can use PowerShell to check compliancy with your security policies).

While checking the roles, you can as well check if each role is required to be installed on Windows Server CORE (and hence if the combination of all (allowed) roles on the server is required to be installed on Windows Server CORE).

To check if a server is installed as Windows Server CORE, you can use the OperatingSystemSKU property of the Win32_OperatingSystem WMI object. The values 12, 13 and 14 refer to Windows Server CORE OS SKU's (Datacenter CORE, Enterprise CORE and Standard Server CORE).

If you want to have a quick check (really superficial) to see if your company's security policy GPOs have been applied, you could check for specific registry values that are set within the policies. I prefer to use the Legal Notice, since that value is mostly unique for each company (if used of course). The values set by GPOs can be easily checked in the registry (for the majority of the settings); for example, the Legal Notice Caption can be found as follows:

$LegalNoticeCaption = Get-ItemProperty -path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -name LegalNoticeCaption

 Another check would be to quickly validate if security updates are being installed; if you want to do a full check (which you should from time to time), use MBSA or equivalent tools. As a quick check, you could just validate when the last update was installed:

$AllQFEs = Get-WmiObject -Namespace "root\CIMV2" -class "win32_QuickFixEngineering" |sort -Property @{Expression={[dateTime]$_.InstalledOn};Ascending=$false}

$lastQFEDate = [datetime]$AllQFEs[0].InstalledOn # check first if $AllQFEs is not empty!

Further, you can find in the script 2 other checks:

  • Check how many users (local or domain) are member of the administrators group (I will explain that in more depth in future)
  • Check if the computer is joined to a domain: this is accomplished by checking the PartOfDomain property on the Win32_ComputerSystem WMI object; the domain property gives you the name of the domain.

Below you can find the full script (including the DNS part of my previous post). For better readability, copy to notepad or PowerShell ISE and validate on your test-environment. (ps: I don't check if DNS is installed or not, so it might fail if DNS is not installed on the server; I leave it up to you to add the code to check first if DNS role is installed before calling the CheckSecCompl-DNS function)

#-------------------------------------------------------------------- # Sample Security Compliancy Check Script for Windows 2008 R2 Roles # Raphael Cox, Microsoft Consulting Services Belgium-Luxembourg #         # January 2011 # # Parameters: # none #--------------------------------------------------------------------           

Function New-CompliancyResult{ Param( [string]$Category, [string]$SubCategory, [string]$setting, [boolean]$IsCompliant ) # create a new object and set the noteproperties using a hash-table $x = New-Object PSObject -Property @{ Computer = (Get-WmiObject -Class Win32_ComputerSystem).name Category = $Category SubCategory = $SubCategory Setting = $setting IsCompliant = $IsCompliant } return $x}

function CheckSecCompl-DNS { $CompliancyResList = @()

 $DNSServer = Get-WmiObject -Namespace "root\MicrosoftDNS" -Class "MicrosoftDNS_Server"

 $CompliancyResList += New-CompliancyResult "DNS" "Server Configuration" "Secure Cache Against Pollution" $DNSServer.SecureResponses

 $DNSZones = Get-WmiObject -Namespace "root\MicrosoftDNS" -Class "MicrosoftDNS_Zone" foreach ($DNSzone in $DNSZones) { $SubCategory = "DNS Zone: " + $DNSzone.Name # To check if a DNS zone only accepts secure dynamic updates, use the property: AllowUpdate # AllowUpdate can have following values: # 0: no dynamic updates allowed # 1: secure and insecure dynamic updates allowed --> not secure # 2: only secure dynamic updates allowed $CompliancyResList += New-CompliancyResult "DNS" $SubCategory "Secure or no dynamic updates" ($DNSzone.AllowUpdate -eq 1) # To check if a DNS zone is Active-Directory-integrated, use the property: DsIntegrated $CompliancyResList += New-CompliancyResult "DNS" $SubCategory "Zone is AD integrated" $DNSZone.DsIntegrated # To check if a DNS zone doesn't allow zone transfers to any computer, use the property: SecureSecondaries # SecureSecondaries can have following values: # 0: Allow zone transfers to any server --> not secure # 1: Allow zone transfers only to specific servers (listed in the name-servers tab) # 2: Allow zone transfers only to specific servers (listed in the zone-transfers tab) # 3: Do not allow Zone Transfers $CompliancyResList += New-CompliancyResult "DNS" $SubCategory "Zone transfers only to known secondaries" ($DNSZone.SecureSecondaries -ne 0) } return $CompliancyResList}

function Getlocalgroupmembers ([string]$localcomputername, [string]$localgroupname) { $groupobj =[ADSI]"WinNT://$localcomputername/$localgroupname" $localmembers = @($groupobj.psbase.Invoke("Members")) $localmembers | foreach {$_.GetType().InvokeMember("AdsPath","GetProperty",$null,$_,$null)} }

function GetAllUserMembers ($group) { # $group is string in format "LDAP://CN=..." $UserList = @() $groupMembers = ([adsi]$group).member foreach ($PrincipalPath in $groupMembers) { $principal = [adsi]"LDAP://$PrincipalPath" if ($principal.SchemaClassName -eq "group") { # if a group, recursively add all members of this group to the list $UserList += GetAllUserMembers $principal.Path } else { # if not a group, it is either user or workstation $UserList += $PrincipalPath } } return $UserList


Function CountAllUsersInLocalGroup ($LocalGroupName) { $Members = Getlocalgroupmembers . $LocalGroupName $countLocalUsers = 0 $MemberInDomain = @() $CheckMembersInOtherDomains=$false if ($Members.count -gt 0) { foreach ($Principal in $Members) { # break up the string of each principal "WinNT://domain\username" $spl = $Principal -split "WinNT://" $UserDomain = ($spl[1] -split "/")[0] $UserName = ($spl[1] -split "/")[1] $domain = [System.DirectoryServices.ActiveDirectory.domain]::GetCurrentdomain() $root = $domain.GetDirectoryEntry() # create object to search in AD $search = [System.DirectoryServices.DirectorySearcher]$root if (-not ($UserDomain -eq (gwmi "Win32_Computersystem").caption) ) #check $userdomain equals local computername { #user, workstation or group is domain member $search.Filter = "(cn=$UserName)" $result = $search.FindOne() if ($result -ne $null) {$MemberInDomain += GetAllUserMembers $result.Path} else { #user is in other domain of the forest or in other forest; not counted here } } else { #local users $countLocalUsers +=1 } } }

    #count all users found locally and in domain (eliminate duplicates) return $countLocalUsers + ($MemberInDomain | sort -Unique).count}

function CheckSecCompl-BaseChecks { Import-module ServerManager

 $CompliancyResList = @() # Get all installed roles $installedRoles = Get-WindowsFeature | where {$_.featureType -eq "Role" -and $_.Installed -eq $true} | select name, DisplayName

    # sample data structure that lists allowed roles, allowed role-combinations and if required for CORE install # This is not a security recommendation, just a sample! $RoleData = @() $RoleData += New-Object PSObject -Property @{Role="AD-Domain-Services";combine=@("DNS"); CORErequired=$true } $RoleData += New-Object PSObject -Property @{Role="Print-Services”;combine=@("DHCP","File-Services"); CORErequired=$true} $RoleData += New-Object PSObject -Property @{Role="File-Services”;combine=@("DHCP","Print-Services"); CORErequired=$true} $RoleData += New-Object PSObject -Property @{Role="DHCP";combine=@(”File-Services”,”Print-Services”); CORErequired=$true} $RoleData += New-Object PSObject -Property @{Role="DNS";combine=@("AD-Domain-Services"); CORErequired=$true}   

    $COREReqForAllRoles = $true foreach ($installedRole in $installedRoles) { $roleFound=$false foreach ($RoleItem in $RoleData) { if ($RoleItem.Role -eq $InstalledRole.Name) { $roleFound=$true break; } } if ($roleFound) { $CompliancyResList += New-CompliancyResult "Base Configuration" "Role Allowed" $installedRole.DisplayName $true foreach ($otherRole in $installedRoles) { if ($ -ne $ { $roleCombStr = $ + " & " + $otherRole.DisplayName if ($RoleItem.combine -contains $ { $CompliancyResList += New-CompliancyResult "Base Configuration" "Role combination allowed" $roleCombStr $true } else { $CompliancyResList += New-CompliancyResult "Base Configuration" "Role combination allowed" $roleCombStr $false } } if ($roleItem.CORERequired -eq $false) { $CoreReqForAllRules = $false} } } else { $CompliancyResList += New-CompliancyResult "Base Configuration" "Role Allowed" $installedRole.DisplayName $false } } # Do all installed roles support Windows Server CORE $OSSKU = (Get-WmiObject -Namespace "root\CIMV2" -class "Win32_OperatingSystem").OperatingSystemSKU $IsCORE = $OSSKU -eq 12-or $OSSKU -eq 13 -or $OSSKU -eq 14

    if ($COREReqForAllRoles) { $CompliancyResList += New-CompliancyResult "Base Configuration" "CORE Install" "Windows CORE Install Required" $IsCore } # Quick check if security policy GPO is applied by checking Legal Notice caption text # Warning: this does not guarantee that a security policy is correctly applied! Other quick checks can be added in the same way $ApprovedLegalNoticeCaption = "You are about to log on to CONTOSO Networks" $SettingText = "Legal Notice Caption set to: " + $ApprovedLegalNoticeCaption $LegalNoticeCaption = Get-ItemProperty -path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -name LegalNoticeCaption if ($LegalNoticeCaption.LegalNoticeCaption -eq "" -or $LegalNoticeCaption.LegalNoticeCaption -eq $null){ $CompliancyResList += New-CompliancyResult "Base Configuration" "Security GPO applied" $SettingText $false } else { if ($LegalNoticeCaption -ne $ApprovedLegalNoticeCaption) { $CompliancyResList += New-CompliancyResult "Base Configuration" "Security GPO settings" $SettingText $false } else { $CompliancyResList += New-CompliancyResult "Base Configuration" "Security GPO settings" $SettingText $true } } # Quick check if Windows Update is enabled by checking time of last update # Warning: this check doesn't guarantee that all Security Updates have correctly been installed and no important ones are missing # you can extend this part, e.g. by checking against a list of must-have hotfixes; for a full check, use MBSA or equivalent tools $AllQFEs = Get-WmiObject -Namespace "root\CIMV2" -class "win32_QuickFixEngineering" |sort -Property @{Expression={[dateTime]$_.InstalledOn};Ascending=$false} $AllowedNrDaysSinceLastUpdate = 40 # 40 Days is NOT a recommendations, just an example $SettingText = "Last update installed less than " + $AllowedNrDaysSinceLastUpdate + " days ago" if ($AllQFEs -eq $null) { $CompliancyResList += New-CompliancyResult "Base Configuration" "Security updates" $SettingText $false } else { $lastQFEDate = [datetime]$AllQFEs[0].InstalledOn if ($lastQFEDate -lt [datetime]::now.AddDays(-30)) { $CompliancyResList += New-CompliancyResult "Base Configuration" "Security updates" $SettingText $false } else { $CompliancyResList += New-CompliancyResult "Base Configuration" "Security updates" $SettingText $false } }   

    # Check number of Administrators on the machine: List members of local Administrators group (local and in domain) # you can extend this part by checking other local groups $PrivGroupToCheck = "Administrators" $MaxNrOfMembers = 5 # a maximum number of 5 admins is just an example;not a security recommendations! $NrOfAdministrators = CountAllUsersInLocalGroup $PrivGroupToCheck $SettingText = "Nr of members of $PrivGroupToCheck <= $MaxNrOfMembers" if ($NrOfAdministrators -le $MaxNrOfMembers) { $CompliancyResList += New-CompliancyResult "Base Configuration" "Privileged Access" $SettingText $true } else { $CompliancyResList += New-CompliancyResult "Base Configuration" "Privileged Access" $SettingText $false } # Check if the computer is joined to domain $WMIComputerSystem = Get-WmiObject -Namespace "root\CIMV2" -class "Win32_ComputerSystem" if ($WMIComputerSystem.PartOfDomain) { $ComputerDomain = $WMIComputerSystem.domain $CompliancyResList += New-CompliancyResult "Base Configuration" "Domain Joined" "Joined to domain ($ComputerDomain)" $true } else { $CompliancyResList += New-CompliancyResult "Base Configuration" "Domain Joined" "Joined to domain (none)" $true } return $compliancyResList}

$CompliancyResList = @()$CompliancyResList += CheckSecCompl-BaseChecks

$CompliancyResList += CheckSecCompl-DNS

$CompliancyResList | select computer, category, subcategory, setting, iscompliant |Out-GridView