Windows Firewall as Code

Introduction

The Windows Firewall provides an important layer of protection and a rich interface to configure it. Unfortunately, at the time of writing, the Active Directory Group Policy section for Windows Firewall with Advanced Security does not allow for the concept of item level filter, resulting in a GPO for each policy with different filtering requirements.

For example, if a handful of servers require FTP ports to be open and another group requires RDP to be allowed, you will have to create two separate GPOs. 

Over time these GPOs really add up.

The solution to this is to do “Firewall as Code” using PowerShell.

Investigation

Well-behaved applications automatically register firewall rules which means that applications will happily continue to work when the firewall is enabled. 

Unfortunately, some applications do not include this as part of their install process. These applications require a manual rule to be configured.

Involve your environment application owners, especially for providing information of running processes and supplying network diagrams. I recommend, however, that you do not directly ask application owners which ports an application needs. I’ve found that if you do that, you will usually get a massive list of irrelevant ports back, mainly because application owners will just give you a list of open ports found on the application servers.

Always check that ports which are required are limited to a specific process. This way you can whitelist the application, allowing all the required ports without actually worrying which protocols or ports are required.

This PowerShell function can be helpful to identify ports used by applications (adapted from http://blogs.microsoft.co.il/scriptfanatic/2011/02/10/how-to-find-running-processes-and-their-port-number/)

function Get-NetworkStatistics 

    $properties = 'Protocol','LocalAddress','LocalPort' 
    $properties += 'RemoteAddress','RemotePort','State','ProcessName','ProcessPath','ProcessCompany','PID' 


    netstat -ano | Select-String -Pattern '\s+(TCP|UDP)' | ForEach-Object { 


        $item = $_.line.split(" ",[System.StringSplitOptions]::RemoveEmptyEntries) 


        if($item[1] -notmatch '^\[::') 
        {
            if (($la = $item[1] -as [ipaddress]).AddressFamily -eq 'InterNetworkV6') 
            { 
               $localAddress = $la.IPAddressToString 
               $localPort = $item[1].split('\]:')[-1] 
            } 
            else 
            { 
                $localAddress = $item[1].split(':')[0] 
                $localPort = $item[1].split(':')[-1] 
            }  


            if (($ra = $item[2] -as [ipaddress]).AddressFamily -eq 'InterNetworkV6') 
            { 
               $remoteAddress = $ra.IPAddressToString 
               $remotePort = $item[2].split('\]:')[-1] 
            } 
            else 
            { 
               $remoteAddress = $item[2].split(':')[0] 
               $remotePort = $item[2].split(':')[-1] 
            }  


            New-Object PSObject -Property @{ 
                PID = $item[-1] 
                ProcessName = (Get-Process -Id $item[-1] -ErrorAction SilentlyContinue).Name
                ProcessPath = (Get-Process -Id $item[-1] -ErrorAction SilentlyContinue).Path
                ProcessCompany = (Get-Process -Id $item[-1] -ErrorAction SilentlyContinue).Company
                Protocol = $item[0] 
                LocalAddress = $localAddress 
                LocalPort = $localPort 
                RemoteAddress =$remoteAddress 
                RemotePort = $remotePort 
                State = if($item[0] -eq 'tcp') {$item[3]} else {$null} 
            } | Select-Object -Property $properties 
        } 
    } 
}

To get a report, run the following after importing the function

Get-NetworkStatistics | Export-Csv -NoTypeInformation .\Ports.csv

Sample export from PowerShell function

Prestage rules

The key to enabling Windows Firewall involves pre-staging firewall rules well in advance of actually flipping the switch. This is achieved with a PowerShell script, executed at an interval on all devices. Conditions within the script selectively create the appropriate rule.

For example, you only want MSSQL firewall rule on servers running MSSQL,

Create PowerShell Script

Although many options exist that would be able to achieve this, I picked PowerShell because of its popularity amongst Windows Administrators.

The PowerShell script to achieve this consists of the following concepts;

  • Environmental Variables
  • Global Variables
  • Local Variables
  • Conditions
  • Rules

Environmental Variables

Specifically, to creating path conditions and rules, it is always better to use Environmental variables if the application is installed into one of these common locations.

To get these variable names, use the following PowerShell command

# List PowerShell's Environmental Variables
Get-Childitem -Path Env:* |Sort-Object Name

Global Variables

Global variables are variables that are used multiple times within the script thus, it makes sense to move these to the top of the file so that they are only evaluated once. The only downside to having multiple instances of these throughout the file is that the script will take longer to execute.

 

Some examples:

Computer account membership

$computerGroupMembers = ([adsisearcher]"(&(objectCategory=computer)(cn=$env:COMPUTERNAME))").FindOne().Properties.memberof -replace '^CN=([^,]+).+$','$1';

Application registry key

$SqlKey = Get-ChildItem -ErrorAction SilentlyContinue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server";

Computer system information

$Computer = Get-WmiObject -Class Win32_ComputerSystem 

Local Variables

When a variable is changed from one condition to another and/or needs to be re-evaluated from one condition to the next, use a local variable.

Conditions

Conditions determine if a particular rule will be applied. That said, a rule without a condition is considered a global rule because it applies to all devices

 

Some examples:

Domain Roles

switch -regex ($computer.DomainRole)
{
    "0|1"
    { #Rules for workstations go here }
    "2|3"
    { #Rules for servers go here }
    "4|5"
    { #Rules for domain controllers go here }
}

Group membership

If ($computerGroupMembers -contains "Group01") { #Rules go here }

Multiple group membership

If ($computerGroupMembers -contains "Group01" -or $computerGroupMembers -contains "Group01") { #Rules go here }

Application binary exists at path using environmental variables

If (Test-Path -Path "$($Env:programfiles)\SomePath\SomeApp.exe"){ #Rules go here }
If (Test-Path -Path "$($Env:SystemDrive)\SomePath\SomeApp.exe "){ #Rules go here }
If (Test-Path -Path "$(${Env:ProgramFiles(x86)})\SomePath\SomeApp.exe") { #Rules go here }

Application binary running (notice the local variable)

$processPath = (Get-Process -Name SomeProcessName).path;
If (Test-Path -Path "$($processPath)") { #Rules go here }

Rules

Each rule name must be unique and descriptive and if applicable to a specific protocol, add the protocol to the rule name within parentheses.

Please note: PowerShell has a powerful CMDLet called New-NetFirewallRule that vastly simplifies the addition of firewall rule. Unfortunately, older versions of PowerShell do not contain it and as such, NETSH is used because it is compatible with more devices. The downside of NETSH is that we first need to delete the rule to ensure we do not create duplicate rules on subsequent executions.

 

Some examples:

Management range that bypass firewall completely

netsh advfirewall firewall delete rule name="Sample Rule Name"
netsh advfirewall firewall add rule name="Sample Rule Name" protocol=any action=allow dir=IN remoteip=10.1.1.0/24,10.2.2.0/24,10.3.3.0/24

TCP single port rule

netsh advfirewall firewall delete rule name="FTP (TCP)"
netsh advfirewall firewall add rule name="FTP (TCP)" protocol=6 action=allow dir=IN localport="21"

TCP multiple port ranges rule

netsh advfirewall firewall delete rule name="Sample Rule Name (TCP)"
netsh advfirewall firewall add rule name="Sample Rule Name (TCP)" protocol=6 action=allow dir=IN localport="900,1000-2000,3000-4000"

TCP and UDP port range rule

netsh advfirewall firewall delete rule name="Sample Rule Name (TCP)"
netsh advfirewall firewall delete rule name="Sample Rule Name (UDP)"
netsh advfirewall firewall add rule name="Sample Rule Name (TCP)" protocol=6 action=allow dir=IN localport="1000-2000"
netsh advfirewall firewall add rule name="Sample Rule Name (UDP)" protocol=17 action=allow dir=IN localport="1000-2000"

Absolute path application rule

netsh advfirewall firewall delete rule name="Sample Rule Name"
netsh advfirewall firewall add rule name="Sample Rule Name" dir=in action=allow program="C:\SomePath\SomeApp.exe"

Multiple combined environmental variable application rules

netsh advfirewall firewall delete rule name="Sample Rule Name 01"
netsh advfirewall firewall delete rule name="Sample Rule Name 02"
netsh advfirewall firewall delete rule name="Sample Rule Name 03"
netsh advfirewall firewall add rule name="Sample Rule Name 01" dir=in action=allow program="$($Env:programfiles)\SomePath\SomeApp.exe"
netsh advfirewall firewall add rule name="Sample Rule Name 02" dir=in action=allow program="$($Env:ProgramFiles(x86))\SomePath\SomeApp.exe"
netsh advfirewall firewall add rule name="Sample Rule Name 02" dir=in action=allow program="$($Env:SystemDrive)\SomePath\SomeApp.exe"

Using local variable

netsh advfirewall firewall delete rule name="Sample Rule Name"
netsh advfirewall firewall add rule name="Sample Rule Name" dir=in action=allow program="$($processPath)"

Predefined rules

netsh advfirewall firewall set rule group="remote desktop" new enable=yes

Firewall.ps1 example

$computerGroupMembers = ([adsisearcher]"(&(objectCategory=computer)(cn=$env:COMPUTERNAME))").FindOne().Properties.memberof -replace '^CN=([^,]+).+$','$1';
$SqlKey = Get-ChildItem -ErrorAction SilentlyContinue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server";

If ($computerGroupMembers -contains "FirewallAllowFTP")
{
netsh advfirewall firewall delete rule name="FTP (TCP)"
netsh advfirewall firewall add rule name="FTP (TCP)" protocol=6 action=allow dir=IN localport="21"
}

If ($computerGroupMembers -contains "FirewallAllowRDP")
{
netsh advfirewall firewall set rule group="remote desktop" new enable=yes
}

If (Test-Path -Path "$($Env:SystemDrive)\App\bin\Engine.exe")
{
netsh advfirewall firewall delete rule name="App01"
netsh advfirewall firewall add rule name="App02" dir=in action=allow program="$($Env:SystemDrive)\App\bin\App.exe"
}

If ($SqlKey -ne $null)
{
netsh advfirewall firewall delete rule name="MSSQL - SQL Server"
netsh advfirewall firewall delete rule name="MSSQL - SQL Admin Connection"
netsh advfirewall firewall delete rule name="MSSQL - SQL Database Management"
netsh advfirewall firewall delete rule name="MSSQL - SQL Service Broker"
netsh advfirewall firewall delete rule name="MSSQL - SQL Debugger/RPC"
netsh advfirewall firewall delete rule name="MSSQL - SQL Server Browse Button Service"
netsh advfirewall firewall delete rule name="MSAS - SQL Analysis Services"
netsh advfirewall firewall delete rule name="MSAS - SQL Browser"
netsh advfirewall firewall delete rule name="MSRS - HTTP"
netsh advfirewall firewall delete rule name="MSRS - SSL"

netsh advfirewall firewall add rule name="MSSQL - SQL Server" dir=in action=allow protocol=TCP localport=1433
netsh advfirewall firewall add rule name="MSSQL - SQL Admin Connection" dir=in action=allow protocol=TCP localport=1434
netsh advfirewall firewall add rule name="MSSQL - SQL Database Management" dir=in action=allow protocol=UDP localport=1434
netsh advfirewall firewall add rule name="MSSQL - SQL Service Broker" dir=in action=allow protocol=TCP localport=4022
netsh advfirewall firewall add rule name="MSSQL - SQL Debugger/RPC" dir=in action=allow protocol=TCP localport=135
netsh advfirewall firewall add rule name="MSSQL - SQL Server Browse Button Service" dir=in action=allow protocol=UDP localport=1433
netsh advfirewall firewall add rule name="MSAS - SQL Analysis Services" dir=in action=allow protocol=TCP localport=2383
netsh advfirewall firewall add rule name="MSAS - SQL Browser" dir=in action=allow protocol=TCP localport=2382
netsh advfirewall firewall add rule name="MSRS - HTTP" dir=in action=allow protocol=TCP localport=80
netsh advfirewall firewall add rule name="MSRS - SSL" dir=in action=allow protocol=TCP localport=443

$SubKeys = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server";
ForEach ($SubKey in $SubKeys)
{
If ($SubKey.Name.Contains("\MSSQL") -And !$SubKey.Name.Contains("\MSSQLServer"))
{
$InstanceName = $SubKey.Name.Split("\")[4]
$RegistryKey = Get-ItemProperty "HKLM:$($SubKey.Name)\Setup" -name SQLBinRoot;
$EXEPath = "$($RegistryKey.SQLBinRoot)\sqlservr.exe";

netsh advfirewall firewall delete rule name="MSSQL - $($InstanceName)"
netsh advfirewall firewall add rule name="MSSQL - $($InstanceName)" dir=in action=allow program="$($EXEPath)"
}
If ($SubKey.Name.Contains("\MSAS"))
{
$InstanceName = $SubKey.Name.Split("\")[4]
$RegistryKey = Get-ItemProperty "HKLM:$($SubKey.Name)\Setup" -name SQLBinRoot;
$EXEPath = "$($RegistryKey.SQLBinRoot)\msmdsrv.exe";

netsh advfirewall firewall delete rule name="MSAS - $($InstanceName)"
netsh advfirewall firewall add rule name="MSAS - $($InstanceName)" dir=in action=allow program="$($EXEPath)"
}
}
}

Implementation

1) Copy Firewall.ps1 to shared location (\\ittelligence.com\NETLOGON\Software\Scripts in this case)

2) Deploy file via GPO Files Preferences

3) Create a scheduled task with GPO Scheduled Tasks running as NT AUTHORITY\SYSTEM

4) Specify the following as options;

Program/scripts: PowerShell.exe

Add arguments (optional): -ExecutionPolicy Bypass -File “%CommonAppdataDir%\ITtelligence\Scripts\Firewall.ps1”

5) After running this GPO for a few days do some spot checks. If rules are creating successfully, enable the firewall.

 

Active Directory – Securely Set Local Account Passwords

How it works

A token is generated for a supplied account with the desired password. 

Example of a token: k8vVeIYZeI+6rkvlvw8eLOEnHK2yTcBfHQP4UEZrCgigcagy7+qt969LISkmHH/7CS5KfVWLEZh8cZMzCkVYGw==

This token (an AES-256 encrypted version of the username and the password) is passed to the SecurelySetPassword tool which is executed at start-up via an Active Directory Group Policy.

The token is decrypted and used to set the password for the specified account to the desired password.

Step 1: Download

Download SecurelySetPassword tool

Step 2: Create and Test Token

1) Run SecurelySetPassword.exe USERNAME PASSWORD (Note how the generated token is different on each run, this is because the value is salted for added security)

2) Copy token. It will be used in the implementation steps

3) To test the token, run SecurelySetPassword.exe TOKEN (Note for a successful test the user needs to exists)

Step 3: Copy SecurelySetPassword.exe to a network share

Copy SecurelySetPassword.exe to a network share accessible by all users (such as NETLOGON share)

Step 4: Implement Active Directory Group Policy

1) Start Microsoft Group Policy Management Console (GPMC.msc

2) Create and link a new Group Policy with the desired scope

3) Browse to Computer Configuration > Preferences > Windows Settings > Files and add a new file object

4) Set the Source files(s) path to the location of SecurelySetPassword.exe (\\ittelligence.com\NETLOGON\Software\SecurelySetPassword\SecurelySetPassword.exe in my case)

5) Set the Destination file to %CommonAppdataDir%\SecurelySetPassword\SecurelySetPassword.exe

6) Browse to Computer Configuration > Preferences > Control Panel Settings > Scheduled Tasks and add a new scheduled task object 

7) On the Triggers tab create new trigger and set to At startup

8) On the Actions tab create a Start a program action to %CommonAppdataDir%\SecurelySetPassword\SecurelySetPassword.exe with token as argument

I hope you found this tutorial useful. You are encouraged to ask questions, report any bugs or make any other comments about it below.

Active Directory – Securely Set Local Account Passwords

Prerequisites: The following assumptions have been made in this tutorial. Readers should have a basic working knowledge of Microsoft Active Directory, SQL Server and Visual Studio software.

Step 1:  Create ACTIVE DIRECTORY SERVICE ACCOUNT

Create an Active directory service account with password reset as well as user account unlock permissions.

Step 2:  Download Visual Studio Project

1) Download the provided source zip file by clicking this link  (See below)

2) Extract and open the project in Visual Studio

Step 3:  Create database

Note: The basic steps for creating the database are listed below. Explaining MS SQL functionality is beyond the scope of this article, but I am happy to answer any questions in the comments section below.

1) From the Open Project in Visual Studio, open ModelSSPR.edmx

2) Right-click on white-space on the diagram page

3) Then select Generate Database from Model as shown below

4) Save the SQL script and use it on Microsoft SQL Server to build the database schema

5) Create an MS SQL user and grant it DB owner rights

step 4:  Modify config file

1) From the open project in Visual Studio

2) Replace the ADConnectionString connection string with the Active Directory LDAP string for the domain used in the Create Active Directory Service Account (Step 1)

3) Replace the SSPREntities connection string with the connection string of the database used in the Create Database (Step 3)

4) Configure ADMembershipProvider to the account created in the Create Active Directory Service Account (Step 1)

5) Replace the appSettings values with the correct information for the domain and account used in the Create Active Directory Service Account (Step 1)

Step 5:  Publish Site

Please Note: Explaining Visual Studio publishing is beyond the scope of this article, but I am happy to answer any questions in the comments section below.

1) From the open project in Visual Studio

2) Publish site with the Visual Studio Publishing wizard

step 6:  Testing Site

Registering password hints

1) Browse to site published in Publish Site (Step 5)

2) Click on Log in

3) Specify the Username and Password for the account to register for self-service password reset.

Note: Username must be in UPN format

4) Create password hints by adding questions and answers

Note: At least four hints need to be specified to utilize the self-service password reset function.

Self-Service Password Reset Request

1) Browse to the site published in the Publish Site (Step 5)

2) Click on Reset Password

3) Enter the Username for the account to reset the password for as shown below

Note: Username must be in UPN format

4) Enter answers to the security questions and provide new password

Note: Three random questions will be selected out of the hints configured

5) Click Reset Password

6) If the password was successfully reset, the following screen will display

I hope you found this tutorial useful. You are encouraged to ask questions, report any bugs or make any other comments about it below.

UserResourceCleanup

INTRODUCTION

A GPO exist that can be configured to automatically delete old user profiles and a process such as CircularLogArchivercan be used to clean up log or old data but what about user data?

UserResouceCleanup can take care of this by monitoring the user data folders and Active Directory.

CONFIGURING USERRESOURCECLEANUP

a) Download and extract UserResourceCleanup.zip (here is VirusTotal scan) to a folder of your choice on the computer which it will be scheduled to run on.

b) Run Configurator.exe (Configurator Editor).

c) On the Encrypt tab, enter the password for the account that will be performing the automated placement task. Encrypt it with key bRK92kDpCqpnPMEtFp1cdJXixgqOqSKFUZ and record encrypted password

d) On the Settings tab, enter the domain information, connection username and the encrypted password recorded in step c. Configure UserNameMatch to a RegEx query that will match user account format. If you do not have a specific format, use .*

e) On the UserFolderLocations tab, specify folders to monitor to redundant user data

f) On the UsersToSkip tab, specify user folders to skip

g) Save configuration files

h) Schedule UserResourceCleanup.ConsoleApp.exe as a scheduled tasks

CONCLUSION

Using this process will keep recover space by removing old/redundant user data, just make sure that you have backups to cheap/slower storage in case you need to recover data.

Automated object placement using AutoAD

IMPLEMENTATION

1) Computer Description Update Process

a) Delegation

To be able to update computer descriptions you need to delegate rights.

Add the following permissions to Active Directory either to the root of the domain or any other Organizational Unit. You would add it to an Organizational Unit if you only want to use this process for some computers

b) Powershell Script

Below is the Powershell script used to update the computer description.

It is important to note that you should not change the format of the message if you are planning to use my automated object placement process.

This script will be used within a group policy in step c

try
    {
        # Get current user name
        $strUserName = $env:username;

        # Get current computer name
        $strComputerName = $env:computername;

        $objADSystemInfo = New-Object -ComObject ADSystemInfo;
        $objType = $objADSystemInfo.GetType();

        # Get current site name
        $strSiteName = $objType.InvokeMember('SiteName', 'GetProperty', $null, $objADSystemInfo, $null);

        # Get current date and time
        $strLogonDate = Get-Date -Format "dd-MM-yyyy HH:mm:ss";

        # Build message
        $strMessage = "$($strUserName) logged in on $($strLogonDate) at $($strSiteName) site";

        # Get computer object from Active Directory
        $strFilter = "(&(objectCategory=Computer)(name=$strComputerName))"
        $objSearcher = New-Object System.DirectoryServices.DirectorySearcher
        $objSearcher.Filter = $strFilter
        $objPath = $objSearcher.FindOne()
        $objComputer = $objPath.GetDirectoryEntry()

        # Update computer object description with message in Active Directory
        $objComputer.InvokeSet("Description", $strMessage)
        $objComputer.CommitChanges()
    }
catch
    {
        throw
    }

c) Group Policy Object

Create a GPO and link it to the root of a domain or Organizational Unit used in step a

Add the PowerShell script from step b as a User Logon script

d) Result

After these steps, notice how the computer descriptions are automatically populated once the users log on to their computers

2) AutoAD

a) Download and extract AutoAD.zip (here is VirusTotal scan) to a folder of your choice on the computer which it will be scheduled to run on.

b) Run Configurator.exe (Configurator Editor).

c) On the Encrypt tab, enter the password for the account that will be performing the automated placement task. Encrypt it with key 2xCJvezFBYWQPBeHy7USdajK55M8skww and record encrypted password

d) On the Settings tab, enter the domain information, connection user name and the encrypted password recorded in step 2c.

Specify which objects AutoAD should create automatically

e) Specify Active Directory information. The format for these are Subnet/Bit Mask|AD Site Name|Computer DN|User DN

Subnet/Bit Mask: The subnet and mask (in bit format) for the specific entry

AD Site: The Active Directory site to which the subnet belongs

Computer DN: The distinguished name of the organizational unit where to move computers to for computer objects in this subnet

User DN: The distinguished name of the organizational unit where to move users to for user objects in this subnet

Please Note: Ensure that you do not allow users/admins to gain any additional permissions by moving users from one container to another. The reason for this is that a user move might be forced to an incorrect OU if descriptions are tampered with. 

f) Specify any user DNs that should be skipped

g) Specify any computer DNs that should be skipped

DEMO EXECUTION

After implementing ComputerDescriptionUpdate.ps1 notice how computer descriptions are automatically updated

AutoAD.exe output

Sites and subnets automatically created by AutoAD

Organizational Units automatically created by AutoAD

Object placement (example 1)

Object placement (example 2)

Object placement (example 3)

Object placement (example 4)

CONCLUSION

Using this process will keep Active Directory organized and objects in the correct Organizational Units

Active Directory Password Reset Tool

BENEFITS

  • Sets a unique, secure password on each password reset
  • Helpdesk employee users do not need to use or install RSAT (at least not for those only resetting passwords)
  • End-users do not get passwords such as Password1 or Company1 and continue with this bad practice by continuing with passwords such as Password2 or Company2

IMPLEMENTATION

1) Download and extract PasswordResetTool.zip (here is VirusTotal scan) to a folder of your choice.

2) Run Configurator.exe (Configurator Editor).

a) On the Settings tab, enter the FQDN and NetBIOS for the domain on for which Password Reset Tool needs to reset passwords for

b) Specify length that passwords should be reset to for user and administrator accounts

c) Save the configuration file

DEMO EXECUTION

Once configuration has been completed, the Password Reset Tool can be executed

Once the Reset Password button is pressed, the specified user account password is reset to a pronounceable, random password

After the process, the connection password and user field are cleared.

RELEVANT CODE

DirectoryEntry directionEntry = new DirectoryEntry(domainPath, domainName + "\\" + connectionUserName, @connectionPassword);
    if (directionEntry != null)
    {
        DirectorySearcher search = new DirectorySearcher(directionEntry);
        search.Filter = "(SAMAccountName=" + userToReset + ")";
        SearchResult result = search.FindOne();
        if (result != null)
        {
            DirectoryEntry userEntry = result.GetDirectoryEntry();
            if (userEntry != null)
            {
                userEntry.Invoke("SetPassword", new object[] { password });
                userEntry.Properties["pwdLastSet"].Value = 0;
                userEntry.Properties["LockOutTime"].Value = 0x0000;
                userEntry.CommitChanges();
            }
        }
    }
    return password;

CONCLUSION

Using initial secure and unique passwords during reset contributes to a far more secure environment.

Active Directory Cleanup Tool (ADCleanup)

INTRODUCTION

ADCleanup is my implementation of a set-and-forget Active Directory cleanup tool. Once this tool is implemented correctly, you never need to worry about dormant accounts ever again.

IMPLEMENTATION

1) Download and extract ADCleanup.zip (here is VirusTotal scan) to a folder of your choice, saved on the computer on which it will be scheduled to run.

2) Create a location in Active Directory to store inactive user accounts and record the distinguished name (DN).

3) Create a location in Active Directory to store inactive computer accounts and record the distinguished name (DN).

4) Run Configurator.exe (Configurator Editor).

a) On the Encrypt tab, enter the password for the account that will be performing the cleanup task. Encrypt it with key 9hOK7AtlGOCRyBtBdhF9pnTQuk8ES176 and record encrypted password

b) On the Settings tab, enter the fully qualified domain name, cleanup account user name and the encrypted password recorded in step 4a

c) Set userCleanup to true to enable the process to clean up user accounts. Set user cleanup parameters

d) Set userDisabledOUto value recorded in step 2

e) Set computerCleanup to true to enable the process to clean up user accounts. Set computer cleanup parameters

f) Set computerDisabledOU to value recorded in step3

g) On the userExcludedDNs tab, specify any distinguished name of an organizational unit that should be excluded from the cleanup process  (+ or INS to add, – or DEL to delete, Enter or double-click to edit)

g) On the computerExcludedDN stab, specify any distinguished name of an organizational unit that should be excluded from the cleanup process (+ or INS to add, – or DEL to delete, Enter or double-click to edit)

h) Schedule ADCleanup.exe to execute via a scheduled task. Upon every execution, the tool will clean up user and computer objects as per your configuration

CONCLUSION

Using this process, (or one similar) will keep Active Directory clean from the unused computer and user objects, and increases server security in the process.

How to manage local account passwords from Active Directory without LAPS

Introduction

Active Directory provided a mechanism to set account password via GPO preferences. Unfortunately this was done with an encrypted cpassword value for which the decryption key was published and subsequently the functionality was blocked.

Later, Local Administrator Password Solution (LAPS), was adopted by Microsoft and it is well suited for the task.

The approach described in this article is an alternative method to LAPS which can be used if the schema extension requirement of LAPS cannot be done. It also allows for multiple account management.

For this solution you will need:

  • File Share
  • SQL Server
  • Group Policy
  • PasswordImporter
  • PasswordViewer

Both PasswordImport and PasswordViewer is available within the same download here

Create share

Create a folder on a server which will hold the passwords reset output files from computers that get the GPO.

It is important to only allow administrators to view files within this folder. Everyone (authenticated users) should only have create/write permission (as shown below). The reason for this is that password will be stored here in clear-text until the PasswordImporter
tool processed it.

Create GPO

Create a GPO and filter it to the computer objects for the computers you wish to manage passwords for.

 

The startup script needs to be set to save password text files to the share created in an earlier step.

Create SQL Table

Use the following SQL script to create a table within SQL to store passwords. Assign a group to this group to SQL and grant it SELECT rights on the Passwords table. Add all your administrators and support personnel into this group.

CREATE TABLE [dbo].[Passwords](
[ID] [int] IDENTITY(1,1) NOT NULL,
[ComputerName] [varchar](255) NOT NULL,
[Password] [varchar](255) NOT NULL,
CONSTRAINT [PK_Passwords] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Below is an example of the passwords stored encrypted within SQL

Setup PasswordImporter

PasswordImporter is an application that reads password files from the share and import is, encrypted, into SQL. After a successful import, the password file is deleted.

Open Configurator and specify Location, ConnectionString and LogFile values. Note that Location and ConnectionString needs to be encrypted with encryption key wL!PIQj%EeWj%L^e$nEpdjFzL0d9%Y1#

 

Schedule this application to run every few minutes to import passwords to SQL database

Setup PasswordViewer

PasswordViewer is an application that shows the specified computers password. This application should be distributed to all administrators and support personnel.

Open Configurator and specify FQDN, NetBIOS and ConnectionString values. Note that ConnectionString needs to provide placeholder values for Domain, Username and
Password, for example:

Data source=YOURSQLSERVER;initial catalog=YOURDATABASE;Integrated Security=SSPI; User Id = [DOMAINNAME\USERNAME]; Password = [PASSWORD]

Phasing in a Group Policy

Group policies can be applied selectively to specific devices with the help of groups. Utilising this, it is possible to phase-in group policies, over a period of time, by randomly adding non-members user or computers at a set interval, to a group filtering a group policy.

Introduction

 

Some settings, especially security related, are postponed indefinitely because no built in phase-in mechanism exists. Sure you can apply a GPO via a group but you have to remember to keep adding members to it and even then, you will
probably not add members in randomly. The process below tackles these issues by automating the GPO phase-in process.

 

Please note: The process below is intended to be configured on the group that is configured as a security filter on a GPO

 

Implementation

 

1) Download and extract ADRandomAddToGroup.zip (here is VirusTotal scan) to a folder of your choice, on the computer which it will be scheduled to run on.

 

2) Get group DN

 

 

3) Run Configurator.exe (Configurator Editor).

 

All configuration is stored within the ADRandomAddToGroup.ConsoleApp.exe file and can be either edited directly or with my Configuration Editor tool.

 

a) On the Settings tab, enter the Group’s distinguished name (DN), recorded in Step 2.

b) Configure whether users or computers should be randomly added, and how many to add at a time. For this example, users are added randomly, five at a time.

 

 

c) On the userExcludedDNs tab, add distinguished names of OUs that contains objects that should never be added to this group. (Use +/-/Insert/Delete/Spacebar to manage entries.)

 

 

d) On the userIncludedDNs tab, add distinguished names of OUs that contains objects that should be added to this group. (Use +/-/Insert/Delete/Spacebar to manage entries.)

 

e) Similarly, included and excluded distinguished names can be configured for computer objects.

f) Save the configuration file and close Configuration Editor.

 

This is how the XML file look for the above setting if you choose to edit it manually.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="userExcludedDNs" type="System.Configuration.NameValueSectionHandler" />
<section name="userIncludedDNs" type="System.Configuration.NameValueSectionHandler" />
<section name="computerExcludedDNs" type="System.Configuration.NameValueSectionHandler" />
<section name="computerIncludedDNs" type="System.Configuration.NameValueSectionHandler" />
</configSections>
<userExcludedDNs>
<add key="35206bba-ff72-40da-ba30-26c93e29470b" value="CN=Account Operators,CN=Builtin,DC=dundermifflin,DC=com" />
<add key="7f8e789c-0978-4378-b718-08501995a307" value="CN=Users,DC=dundermifflin,DC=com" />
</userExcludedDNs>
<userIncludedDNs>
<add key="UserExclusion2" value="CN=Users,DC=dundermifflin,DC=com" />
<add key="091bb899-58b4-427d-a5a9-da29dde49f0a" value="OU=Test Users,OU=Accounts,OU=Dunder Mifflin,DC=dundermifflin,DC=com" />
</userIncludedDNs>
<computerExcludedDNs />
<computerIncludedDNs />
<appSettings>
<add key="groupDN" value="CN=DG-Tier2Users,OU=Delegation Groups,OU=Groups,OU=Dunder Mifflin,DC=dundermifflin,DC=com" />
<add key="userAddToGroup" value="true" />
<add key="userAddToGroupLimit" value="5" />
<add key="computerAddToGroup" value="false" />
<add key="computerAddToGroupLimit" value="0" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>

Demo Execution

 

1) Output for the ADRandomAddToGroup.ConsoleApp.exe command

 

 

2) Random members added to group after first execution

 

 

3) Random members added to group after second execution

 

 

CODE

 

Here is the section in the code that manages the Random Group Members.

try
{
if (string.IsNullOrEmpty(domainFQDN))
{
domainFQDN = ActiveDirectory.GetFQDN(groupDN);
}

DirectoryEntry group;
if (!string.IsNullOrEmpty(domainUserName))
{
group = new DirectoryEntry("LDAP://" + domainFQDN + "/" + groupDN, domainUserName, domainPassword);
}
else
{
group = new DirectoryEntry("LDAP://" + domainFQDN + "/" + groupDN);
}

//Connect to domain
DirectoryEntry de = new DirectoryEntry("LDAP://" + domainFQDN, domainUserName, domainPassword);
if (!string.IsNullOrEmpty(domainUserName))
{
de = new DirectoryEntry("LDAP://" + domainFQDN, domainUserName, domainPassword);
}
else
{
de = new DirectoryEntry("LDAP://" + domainFQDN);
}

if (userAddToGroup)
{
//Loop through users
try
{                      
using (DirectorySearcher ds = new DirectorySearcher())
{
SortOption sortOption = new SortOption("objectGUID", SortDirection.Ascending);

ds.SearchRoot = de;
ds.PageSize = 1000;
ds.Filter = "(&(objectCategory=person)(objectClass=user))";
ds.PropertiesToLoad.Add("memberOf");
ds.PropertiesToLoad.Add("distinguishedName");
ds.PropertiesToLoad.Add("description");
ds.PropertiesToLoad.Add("userAccountControl");
ds.PropertiesToLoad.Add("lastLogonTimestamp");
ds.PropertiesToLoad.Add("lastLogon");
ds.PropertiesToLoad.Add("pwdLastSet");
ds.PropertiesToLoad.Add("whenCreated");
ds.PropertiesToLoad.Add("objectGUID");
ds.SearchScope = SearchScope.Subtree;
ds.Sort = sortOption;

SearchResultCollection result = ds.FindAll();

int userAddToGroupCount = 0;

foreach (SearchResult sr in result)
{
try
{
string distinguishedName = "";
if (sr.Properties.Contains("distinguishedName"))
{
distinguishedName = sr.Properties["distinguishedName"][0].ToString();
}

DirectoryEntry user = sr.GetDirectoryEntry();
if ((!ActiveDirectory.EntryInDN(user, userExcludedDNs)) && (ActiveDirectory.EntryInDN(user, userIncludedDNs)))
{
if (userAddToGroupCount < userAddToGroupLimit)
{
if (ActiveDirectory.AddToGroup(group, distinguishedName))
{
//Increase count
userAddToGroupCount++;

//Log and display
Logging.LogAndDisplay("Informational", "Added user '" + distinguishedName + "' to group '" + groupDN + "'", logFile);
}
}
//else
//{
//    //Verbose
//    Logging.LogAndDisplay("Informational", "[NOT REAL] Added user '" + distinguishedName + "' to group '" + groupDN + "'", logFile);
//}
}
}
catch(Exception ex)
{
//Item error
Logging.LogAndDisplay("Warning", ex.Message, logFile);
}
}
}
}
catch(Exception ex)
{
//Catch global user error
Logging.LogAndDisplay("Error", ex.Message, logFile);
}
}

if (computerAddToGroup)
{
//Loop through computers
try
{
using (DirectorySearcher ds = new DirectorySearcher())
{
SortOption sortOption = new SortOption("objectGUID", SortDirection.Ascending);

ds.SearchRoot = de;
ds.PageSize = 1000;
ds.Filter = "(objectCategory=computer)";
ds.PropertiesToLoad.Add("memberOf");
ds.PropertiesToLoad.Add("distinguishedName");
ds.PropertiesToLoad.Add("description");
ds.PropertiesToLoad.Add("userAccountControl");
ds.PropertiesToLoad.Add("lastLogonTimestamp");
ds.PropertiesToLoad.Add("lastLogon");
ds.PropertiesToLoad.Add("pwdLastSet");
ds.PropertiesToLoad.Add("whenCreated");
ds.PropertiesToLoad.Add("objectGUID");
ds.SearchScope = SearchScope.Subtree;
ds.Sort = sortOption;

SearchResultCollection result = ds.FindAll();

int computerAddToGroupCount = 0;

foreach (SearchResult sr in result)
{
try
{
string distinguishedName = "";
if (sr.Properties.Contains("distinguishedName"))
{
distinguishedName = sr.Properties["distinguishedName"][0].ToString();
}

DirectoryEntry computer = sr.GetDirectoryEntry();
if ((!ActiveDirectory.EntryInDN(computer, computerExcludedDNs)) && (ActiveDirectory.EntryInDN(computer, computerIncludedDNs)))
{
if (computerAddToGroupCount < computerAddToGroupLimit)
{
if (ActiveDirectory.AddToGroup(group, distinguishedName))
{
//Increase count
computerAddToGroupCount++;

//Log and display
Logging.LogAndDisplay("Informational", "Added computer '" + distinguishedName + "' to group '" + groupDN + "'", logFile);
}
}
//else
//{
//    //Verbose
//    Logging.LogAndDisplay("Informational", "[NOT REAL] Added computer '" + distinguishedName + "' to group '" + groupDN + "'", logFile);
//}
}
}
catch (Exception ex)
{
//Item error
Logging.LogAndDisplay("Warning", ex.Message, logFile);
}
}
}
}
catch(Exception ex)
{
//Catch global computer error
Logging.LogAndDisplay("Error", ex.Message, logFile);
}
}
}
catch(Exception ex)
{
//Catch global group error
Logging.LogAndDisplay("Error", ex.Message, logFile);
}

 

Conclusion

 

Using this process, it is easy to set and forget a GPO so that it randomly adds to the scope of management without manual intervention.

 

 

Strategy to centrally manage Local Administrators group from Active Directory

Uncontrolled local administrators groups within any organisation pose a huge security risk. Because these groups are locally managed it becomes difficult to audit and maintain them.

Introduction

 

Active Directory does provide mechanisms such as Group Policy Preferences, Restricted Groups etc. which can manage groups centrally. The issue is that local Administrators groups can differ vastly from computer to computer and because of this it is difficult
to standardize settings within a policy. This leads to over permissions because these individual permissions are painful to define and a higher privileged than required is usually assigned.

 

To make it worse, in a mature environment you first need visibility on current members and chances are that you would need to rely on other parties to rectify or change these because of the initial requirements.

 

In this article, I explain my approach to centrally manage Local Administrators group from Active Directory using a custom developed application.

 

Definitions

 

Local Administrators group The local built-in Administrators group found on each computer.
AD Local Admin group An Active Directory group for each computer that is added to the Local Administrators group of the computer. Its purpose is to hold all the members intended to have Administrative permission to a particular computer.
Local Administrator Permission Referring to the actual permissions given to Administrators.

 

Process Flow

 

Computer OU Loop

Application enumerates computers in the computer OUs specified and adds them to a collection.

 

Computer Loop

Creates a computer AD Local Admin group for each computer if it doesn’t already exist.

 

Local Admin Member Loop

Add members of Local Administrators group to AD Local Admin group. If the member is already part of the AD Local Admin group, remove it from computer’s Local Administrators group. It is important to note that these
two actions will not occur within same execution to allow Kerberos tokens to update.

 

 

 

 

 

Benefits

 

  • Rebuilding computers with the same computer name will restore all administrators.
  • Enable auditing of users with local Administrators permissions without directly querying computer.
    • AD Local Admin group members can be queried directly from Active Directory without connecting to remote computers.
  • Enforce a stronger password policy for the user accounts with Administrator permission to certain computers.
    • Add required AD Local Admin group to a group defined in a fine-grained password policy
  • Remove dormant/orphaned entries.
  • Central management of local administrator permissions.
    • Simply add users or groups (preferably role groups) to the AD Local Admin group to give local Administrator permissions on computers.
  • Any new members will automatically be removed from Local Administrators group and added to the AD LocalAdmins group
  • After implementation, local administrator permission can be enforced via AD.

 

 

Implementation

 

1) Download and extract
AdminGroups.zip (here is VirusTotal scan)
to a folder of your choice on the computer on which it will be scheduled to run.

2) Create a location in Active Directory to create AD Local Admin groups in and record the distinguished name (DN).

 

 

3) Record all the Computer OU’s distinguished name (DN) for the computers that AD Local Admin groups need to be created for. These can be workstations or servers but generally this in only done for servers.

 

 

4) Run

Configurator.exe (Configurator Editor).

 

All configuration is stored within the AdminGroups.ConsoleApp.exe.config file and can be either edited directly or with my Configuration Editor tool.

 

a) On the Settings tab, enter the Admin Group OU’s distinguished name (DN), recorded in Step 2.

b) Enter the prefix that should be used when creating AD Local Admin Group.

c) Enter the suffix that should be used when creating AD Local Admin Group.

d) Optionally add a Group Name to add the computer account to on execution. This is useful if you want other policies to trigger or if you want to see process of the AdminGroups tool.

 

 

e) On the RemoveUsersFromLocalAdmins tab, enter usernames of local accounts that you want to automatically remove from the Local Administrators group.
Use this if redundant local user account with Administrators rights exist, perhaps accounts created as part of the imaging/build process. (Use +/-/Insert/Delete/Spacebar to manage entries.)

 

 

f) On the ComputerOUs tab, enter the Computer OU’s distinguished name (DN), recorded in Step 3. (Use +/-/Insert/Delete/Spacebar to manage entries.)

 

 

g) Add any groups that should not be automatically synced and removed from the AD Local Admins groups, for example, Global Admins. (Use +/-/Insert/Delete/Spacebar to manage entries.)

 

 

h) Save configuration file and close Configuration Editor.

 

This is how the XML file look for the above setting if you choose to edit it manually.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="RemoveUsersFromLocalAdmins" type="System.Configuration.NameValueSectionHandler" />
<section name="ComputerOUs" type="System.Configuration.NameValueSectionHandler" />
<section name="ExcludedDomainGroups" type="System.Configuration.NameValueSectionHandler" />
</configSections>
<ComputerOUs>
<add key="e91e156f-22fe-4b45-8c9e-0863b448cf6f" value="OU=Servers,OU=Computers,OU=Dunder Mifflin,DC=dundermifflin,DC=com" />
<add key="f21a460b-fd05-4790-ae74-9fb4562d6d0b" value="OU=Workstations,OU=Computers,OU=Dunder Mifflin,DC=dundermifflin,DC=com" />
</ComputerOUs>
<RemoveUsersFromLocalAdmins />
<ExcludedDomainGroups>
<add key="c23fa115-91c4-407c-926b-7946430f93df" value="DG-GlobalServerAdmins" />
<add key="38184183-4d32-4554-a343-bb1c333faef3" value="DG-GlobalWorkstationAdmins" />
</ExcludedDomainGroups>
<appSettings>
<add key="AdminGroupOU" value="OU=Admin Groups,OU=Delegation Groups,OU=Groups,OU=Dunder Mifflin,DC=dundermifflin,DC=com" />
<add key="Prefix" value="DG-" />
<add key="Suffix" value="_LocalAdmins" />
<add key="AddComputerToADGroup" value="" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>

 

i) Schedule AdminGroups.ConsoleApp.exe to run one every day. Remember to set the running account to an account with rights

 

Demo Execution

 

1) Below is screenshots of my three demo computers’ Local Administrators groups pre-execution.

 

Windows 7 original Local Administrators Group

Windows 10 original Local Administrators Group




Windows 2016 original Local Administrators Group


2) Below are screenshots of AdminGroup output and my three demo computers’ Local Administrators groups after first execution

 

AdminGroups output after first execution

Windows 7 Local Administrators Group after first execution

Windows 10 Local Administrators Group after first execution

Windows 2016 Local Administrators Group after first execution

 

3) Below is screenshots of AdminGroup output and my three demo computers’ Local Administrators groups after second execution.

 

AdminGroups output after second execution

Windows 7 Local Administrators Group after second execution

Windows 10 Local Administrators Group after second execution

Windows 2016 Local Administrators Group after second execution

 

Where did members go? They are now safely within the AD Local Admins groups.

 

Windows 7 AD Local Admins Group

Windows 10 AD Local Admins Group

Windows 2016 AD Local Admins Group

 

Code

 

Here is the section in code that manages the AD Local Admin Groups.

public static bool CreateAdminGroup(string DomainName, Models.Computer Computer, string AdminGroupOU, string AdminGroupName, List < string > ExcludedDomainGroups, out string Message) {
	Message = "";
	string message = "";
	try {
		//If this fails, no point in continueing
		if (ActiveDirectory.CreateActiveDirectoryGroup(AdminGroupOU, AdminGroupName, out message)) {
			//Add command output to return message
			Message = Message + System.Environment.NewLine + message;

			//If this fails, no point in continueing
			if (ActiveDirectory.RemotelyAddDomainGroupToLocalGroup(Computer.ComputerName, DomainName, AdminGroupName, "administrators", out message)) {
				//Add command output to return message
				Message = Message + System.Environment.NewLine + message;

				List < Models.GroupMember > groupMembers = new List < Models.GroupMember > ();

				//If this fails, no point in continueing
				if (ActiveDirectory.RemotelyGetLocalGroupMembers(Computer.ComputerName, "Administrators", true, false, ref groupMembers, out message)) {
					//Add command output to return message
					Message = Message + System.Environment.NewLine + message;

					foreach(Models.GroupMember groupMember in groupMembers) {
						//Handle excluded domain groups
						if ((!AdminGroupName.EndsWith(groupMember.GroupMemberName)) && (groupMember.GroupMemberName != "Domain Admins") && (!ExcludedDomainGroups.Contains(groupMember.GroupMemberName))) {
							if (ActiveDirectory.AddMemberToActiveDirectoryGroup(AdminGroupOU, AdminGroupName, groupMember.GroupMemberName, out message)) {
								//Add command output to return message
								Message = Message + System.Environment.NewLine + message;

								if (message.EndsWith("Group already exists")) {
									ActiveDirectory.RemotelyRemoveDomainGroupFromLocalGroup(Computer.ComputerName, DomainName, groupMember.GroupMemberName, "administrators", out message);

									//Add command output to return message
									Message = Message + System.Environment.NewLine + message;
								}
							} else {
								//Add command output to return message
								Message = Message + System.Environment.NewLine + message;
							}
						}
					}
				} else {
					//Add command output to return message
					Message = Message + System.Environment.NewLine + message;
				}
			} else {
				//Add command output to return message
				Message = Message + System.Environment.NewLine + message;
			}
		} else {
			//Add command output to return message
			Message = Message + System.Environment.NewLine + message;
		}

		return true;
	} catch (Exception ex) {
		Message = Message + System.Environment.NewLine + ex.Message;
		return false;
	}
}

 

Conclusion

 

Using this process, or one similar will give better visibility on existing users with Local Administrators permissions and the ability to better manage them.