Local Admin Password Maintainer

by Ryan 18. February 2015 17:02

Active Directory is great for robust, centralized management of a large amount of I.T. assets.  But even once you have Active Directory, you're still left with that problem of what to do with local administrator accounts on all of the domain members.  You probably don't want to disable the local admin account, because you'll need it in case the computer is ever in a situation where it can't contact a domain controller.  But you don't have a good way of updating and maintaining the local Administrator password across your entire environment, either.  Everyone knows better than to use Group Policy Preferences to update the local administrator password on domain members, as it is completely unsecure.  Most other solutions involve sending the administrator passwords across the network in clear-text, require an admin to manually run some scripts or software every time that may not work well in complicated networks, and they still leave you with the same local administrator password on every machine... so if an attacker knocks over any one computer in your entire domain, he or she now has access to everything.

This is the situation Local Admin Password Maintainer seeks to alleviate.  LAPM easily integrates into your Active Directory domain and fully automates the creation of random local administrator passwords on every domain member.  The updated password is then transmitted securely to a domain controller and stored in Active Directory.  Only users who have been given the appropriate permissions (Domain Administrators and Account Operators, by default) may view any password.

The solution is comprised of two files: Install.ps1, which is the one-time install script, and LAPM.exe, an agent that will periodically (e.g., once a month,) execute on all domain members.  Please note that these two files will always be digitally signed by me.

Minimum Requirements

  • Active Directory. You need to be a member of both Domain Admins and Schema Admins to perform the install. You must perform the installation on the forest schema master.
  • Forest and domain functional levels of 2008 or better. This software relies on a feature of Active Directory (confidential attributes) that doesn't technically require any certain forest or domain functional level, but enforcing this requirement is an easy way of ensuring that all domain controllers in your forest are running a modern version of Windows.
  • I do not plan on doing any testing of either the install or the agent on Windows XP or Server 2003.  I could hypothetically make this work on XP/2003 SP1, but I don't want to.  If you're still using those operating systems, you aren't that concerned with security anyway.
  • A Public Key Infrastructure (PKI,) such as Active Directory Certificate Services, or otherwise have SSL certificates installed on your domain controllers that enable LDAP over SSL on port 636.  This is because LAPM does not allow transmission of data over the network in an unsecure manner.  It is possible to just bang out some self-signed certificates on your domain controllers, and then distribute those to your clients via Group Policy, but I do not recommend it.
  • The installer requires Powershell 4. Which means you need Powershell 4 on your schema master. Which means it needs to be 2008 R2 or greater.  I could port the install script to an older version of Powershell, but I haven't done it yet.
  • The Active Directory Powershell module. This should already be present if you've met the requirements thus far.
  • The Active Directory Web Service should be running on your DCs. This should already be present if you've met the requirements thus far.
  • LAPM.exe (the "agent") will run on anything Windows Vista/Server 2008 or better, 32 or 64 bit.  I just don't feel like porting it back to XP/2003 yet.

COPYRIGHT AND DISCLAIMER NOTICE:

Copyright ©2015 Joseph Ryan Ries. All Rights Reserved.

IN NO EVENT SHALL JOSEPH RYAN RIES (HEREINAFTER REFERRED TO AS 'THE AUTHOR') BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND/OR ITS DOCUMENTATION, EVEN IF THE AUTHOR IS ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS IS". THE AUTHOR HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.


Installation Instructions

  • Download the installation package found below, and unzip it anywhere on your Active Directory domain controller that holds the Schema Master FSMO role.  (Use the netdom query fsmo command if you forgot which DC is your Schema Master.)
  • If necessary, use the Unblock-File Powershell cmdlet or use the GUI to unblock the downloaded zip file.
  • You can verify the integrity of the downloaded files like so:


  • If you need to change your Powershell execution policy in order to run scripts on your DC, do so now with Set-ExecutionPolicy RemoteSigned.
  • Execute the Install script by typing .\Install.ps1 in the same directory as the script and LAPM.exe.

  • The installation script will perform several prerequisite checks to ensure your Active Directory forest and environment meet the criteria. It will also create a log file that stores a record of everything that takes place during this install session.  If you see any red [ERROR] text, read the error message and try to correct the problem that is preventing the install script from continuing, then try again. (E.g. SSL certificate not trusted, you're not on the Schema Master, etc.)  It's important that you read and consider the warning text, especially the part about how extending the Active Directory schema is a permanent operation.
  • Type yes at the warning prompt to commit to the installation.

  • The installation will now make a small schema modification by adding the LAPMLocalAdminPassword attribute to the Active Directory schema, adding that attribute to the computer object, and then adding an access control entry (ACE) to the root of the domain that allows the SELF principal the ability to write to that attribute.  That means that a computer has the right to modify its own LAPMLocalAdminPassword attribute, but not the attribute of another computer. (A computer does not have the ability to read its own LAPMLocalAdminPassword attribute. It is write-only.)

  • Finally, the install script copies LAPM.exe to the domain's SYSVOL share. This is so all domain members will be able to access it.
  • You are now done with the script and are in the post-installation phase.  You have one small thing left to do.
  • Open Group Policy Management on your domain controller.

  • Create a new GPO and link it to the domain:

  • Name the new GPO Local Admin Password Maintainer.
  • Right click on the new GPO and choose Edit. This will open the GPO editor.
  • Navigate to Computer Configuration > Preferences > Control Panel Settings > Scheduled Tasks.

  • Right-click in the empty area and choose New > Scheduled Task (At least Windows 7).

  • Choose these settings for the new scheduled task. It is very important that the scheduled task be run as NT Authority\System, also known as Local System.


  • This task will be triggered on the first of every month.  It's advisable to configure the random delay shown in the screenshot above, as this will mitigate the flood of new password uploads to your domain controllers on the first of the month.

  • For the program to execute, point to \\YourDomain\SYSVOL\YourDomain\LAPM.exe. Remember that the second "YourDomain" in the path is a reparse point/symlink that looks like "domain" if you view it in File Explorer.  For the optional argument, type BEGIN_MAGIC, in all capital letters.  It is case sensitive.
  • Lastly, the "Remove this item when it is no longer applied" setting is useful.  Unchecking "allow this task to be run on demand" can also be useful.  As an administrator, you have some leeway here to do what makes the most sense for your environment.  You might even choose to scope this GPO to only a certain OU if you only want a subset of the members of your domain to participate in Local Admin Account Maintainer.

  • Click OK to confirm, and you should now have a new scheduled task that will execute on all domain members.
  • Close the Group Policy editor.

Don't worry if the scheduled task also applies to domain controllers.  LAPM.exe detects whether it is running on a domain controller before it does anything, and exits if it is.


It also doesn't matter what the local administrator's name is, in case the account has been renamed. LAPM uses the SID.

LAPM logs successes and failures to the Windows Application event log.  Here is an example of what you might see if a client can't connect to a DC for some reason, like if SSL certificates aren't configured correctly:

In an event like this, LAPM.exe exits before changing the local administrator password, so the password will just stay what it was until the next time the scheduled job runs.

LAPM will generate a random, 16-character long password.  The "randomness" comes from the cryptographically secure PRNG supplied by the Windows API.

Success looks like this:


Now, notice that the standard domain user "Smacky the Frog" is unable to read the LAPMLocalAdminPassword attribute from Active Directory:

However, a Domain Administrator or Account Operator can!

Of course, you can also see it in the GUI as well, with Active Directory Users and Computers with advanced view turned on, for example.

So there you have it. Be smart, test it out in a lab first, and then enjoy your 30-day, random rotating local admin passwords!

As I continue to update this software package, new versions will be published on this page.

Download:

LAPM-1.0.zip (54.4KB)


One Way of Exporting Nicer CSVs with Powershell

by Ryan 16. December 2014 11:12

One of the ever-present conundrums in working with computers is that data that looks good and easily readable to a human, and data that is easy and efficient for a computer to process, are never the same.

In Powershell, you see this "immutable rule" manifest itself in that, despite all the various Format-* cmdlets available to you, some data will just never look good in the console.  And if it looks good in the console, chances are you've mangled the objects so that they've become useless for further processing over the pipeline.  This is essentially one of the Powershell "Gotcha's" espoused by Don Jones, a term that he refers to as "Format Right."  The principal is that if you are going to format your Powershell output with a Format-* cmdlet, you should always do so at the end of the statement (e.g., on the right side.)  The formatting should be the last thing you do in an expression, and you should never try to pass something that has been formatted over the pipeline.

CSV files, in my opinion, are a kind of happy medium, because they are somewhat easy for humans to read (especially if the human has an application like Microsoft Excel or some such,) and CSV files are also relatively easy for computers to read and process.  Therefore, CSVs are a popular format for transporting data and feeding it to computers, while still being legible to humans.

When you use Export-Csv to write a bunch of objects out to a CSV file:

# Get Active Directory groups, their members, and memberships:
Get-ADGroup -Filter * -SearchBase 'CN=Users,DC=domain,DC=local' -Properties Members,MemberOf | `
    Select Name, Members, MemberOf | `
    Export-Csv -NoTypeInformation -Path C:\Users\ryan\Desktop\test.csv 

And those objects contain arrays or lists as properties, you'll get something like this in your CSV file:

"Name","Members","MemberOf"
"MyGroup","Microsoft.ActiveDirectory.Management.ADPropertyValueCollection","Microsoft.ActiveDirectory.Management.ADPropertyValueCollection"

Uh... that is not useful at all.  What's happened is that instead of outputting the contents of the Active Directory group members and memberOf attributes, which are collections/arrays, Powershell has instead output only the names of the .NET types of those collections.

What we need is a way to expand those lists so that they'll go nicely into a CSV file.  So I usually do something like the script excerpt below.  This is just one possible way of doing it; I by no means claim that it's the best way or the only way.

#Get all the AD groups:
$Groups = Get-ADGroup -Filter * -SearchBase 'OU=MyOU,DC=domain,DC=com' -Properties Members,MemberOf

#Create/initialize an empty collection that will contain a collection of objects:
$CSVReadyGroups = @()

#Iterate through each one of the groups:
Foreach ($Group In $Groups)
{
    #Create a new object to hold our "CSV-Ready" version of the group:
    $CSVReadyGroup = New-Object System.Object #Should probably be a PSObject
    #Add some properties to the object.
    $CSVReadyGroup | Add-Member -Type NoteProperty -Name 'Name'     -Value  $Group.Name
    $CSVReadyGroup | Add-Member -Type NoteProperty -Name 'Members'  -Value  $Null
    $CSVReadyGroup | Add-Member -Type NoteProperty -Name 'MemberOf' -Value  $Null

    # If the group has any members, then run the code inside these brackets:
    If ($Group.Members)
    {
        # Poor-man's serialization.
        # We are going to convert the array into a string, with NewLine characters 
        # separating each group member. Could also be more concise just to cast
        # as [String] and do  ($Group.Members -Join [Environment]::NewLine)

        $MembersString = $Null
        Foreach ($GroupMember In $Group.Members)
        {
            $MembersString += $GroupMember + [Environment]::NewLine
        }
        #Trim the one extra newline on the end:
        $MembersString = $MembersString.TrimEnd([Environment]::NewLine)
        #Add to our "CSV-Ready" group object:
        $CSVReadyGroup.Members = $MembersString
    }

    # If the group is a member of any other groups, 
    # then do what we just did for the Members:
    If ($Group.MemberOf)
    {
        $MemberOfString = $Null
        Foreach ($Membership In $Group.MemberOf)
        {
            $MemberOfString += $Membership + [Environment]::NewLine
        }
        $MemberOfString = $MemberOfString.TrimEnd([Environment]::NewLine)
        $CSVReadyGroup.MemberOf = $MemberOfString
    }

    #Add the object we've created to the collection:
    $CSVReadyGroups += $CSVReadyGroup
}

#Output our collection:
$CSVReadyGroups | Export-Csv -NoTypeInformation -Path C:\Users\ryan\Desktop\test.csv

Now you will have a CSV file that has readable arrays in it, that looks good when you open it with an application such as Excel.

Have You Been Pwned by CVE-2014-6324/MS14-068?

by Ryan 19. November 2014 08:11

In case you haven't heard, there is a critical [Windows implementation of] Kerberos bug that you need to be updating, right now.

More information on the vulnerability can be found here.

In the "Detection Guidance" section of the above blog post, you will see that you can detect if the vulnerability has been exploited on an unpatched machine by analyzing the Security event logs. Specifically, looking at Event ID 4624 logon events, and taking note that the "Security ID" and "Account Name" fields in that event description match.  If they don't, chances are high that you have been a victim of a privilege escalation attack.

I whipped up a detection script to check all the domain controllers:

#Requires -Module ActiveDirectory
Set-StrictMode -Version Latest
Get-Job | Remove-Job -Force
[String]$DomainName = $(Get-ADDomain).Name
$DCs = $(Get-ADDomain).ReplicaDirectoryServers

:NextDC Foreach ($DC In $DCs)
{
    Start-Job -ScriptBlock {
        Param($DC)
        [Int]$PotentialMS14068s = 0
        Write-Output "Fetching Security event log from $DC."
        Try
        {
            $Events = Get-EventLog -LogName Security -InstanceId 4624 -ComputerName $DC -ErrorAction Stop
        }
        Catch
        {
            Write-Error "An error occurred while reading event log from $DC.`r`n$($_.Exception.Message)"
        }

        :NextEvent Foreach ($Event In $Events)
        {            
            $MessageLines  = $Event.Message -Split [Environment]::NewLine
            
            [String]$SecurityID    = [String]::Empty
            [String]$AccountName   = [String]::Empty
            [String]$AccountDomain = [String]::Empty

            # Server 2012 Format
            If ($MessageLines[13].Trim() -Like 'Security ID:*')
            {
                $SecurityID    = ($MessageLines[13].Trim() -Split ':')[1].Trim()
                $AccountName   = ($MessageLines[14].Trim() -Split ':')[1].Trim()
                $AccountDomain = ($MessageLines[15].Trim() -Split ':')[1].Trim() 
            }
            
            # Server 2008 R2 Format
            If ($MessageLines[11].Trim() -Like 'Security ID:*')
            {
                $SecurityID    = ($MessageLines[11].Trim() -Split ':')[1].Trim()
                $AccountName   = ($MessageLines[12].Trim() -Split ':')[1].Trim()
                $AccountDomain = ($MessageLines[13].Trim() -Split ':')[1].Trim()
            }

            If (($SecurityID -EQ [String]::Empty) -OR ($AccountName -EQ [String]::Empty) -OR ($AccountDomain -EQ [String]::Empty))
            {
                Write-Error "Event log message format unrecognized on $DC!"
                $Event | Format-List
                Break NextEvent
            }

            If ($AccountDomain -Like $DomainName -And $SecurityID -NotLike 'S-1-5-18')
            {
                $SID = New-Object System.Security.Principal.SecurityIdentifier($SecurityID)
                $Username = $SID.Translate([System.Security.Principal.NTAccount])        
                If ($Username -Like '*\*')
                {
                    $Username = ($Username -Split '\\')[-1]
                }
                If ($Username -Like '*@*')
                {
                    $Username = ($Username -Split '@')[0]
                }
                If ($Username -NE $AccountName)
                {
                    $Event | Format-List
                    $PotentialMS14068s++
                }        
            }
        }
        Write-Output "Finished with $DC. $PotentialMS14068s interesting events found."
    } -ArgumentList $DC
}

While ($(Get-Job -State Running).Count -GT 0)
{
    Get-Job -State Completed | Receive-Job   
    Start-Sleep -Seconds 10 
}

The script uses Powershell jobs to achieve some parallelism, because if you have more than one or two domain controllers in your environment, this quickly becomes a Herculean, time-consuming task.  The script will display potential security event log events that may indicate exploits currently being used in your environment.

Delegating the Permissions for Service Accounts to Dynamically Register Their Own SPNs #274

by Ryan 26. September 2014 19:00

I often use this blog as my own personal scratch space... if any of my writings here end up helping anyone else, that is a major bonus, but I can't count the number of times I've been working on some technical issue and purposely searched through my own articles looking for a Powershell script or something that I vaguely remembered writing that might help with whatever I'm working on at the moment.

And that's why I was really surprised when I searched this blog today for SPN and servicePrincipalName and got nothing.  How have I not written anything about SPNs before!?  We're about to fix that...

Service Principal Names.  A really simple concept that seems, inexplicably, to blow some people's minds.  (Or maybe just make them doze off.)  And more often than not, when I look into Active Directory environments that use SQL Server, IIS, etc., the admins and owners have usually forgotten about SPNs.  And who cares about SPNs anyway? I mean, the application works fine without them, right?  I guess... if you don't mind having to use crummy old NTLM authentication when you could be using swanky Kerberos instead!

I'm going to focus specifically on Microsoft SQL Server and how it uses Service Principal Names today.  I wrote "#274" in the title of this post because there are many different ways to go about delegating these permissions, and I'm just going to present one possible way.  (A way that I think is better than how I have seen other people do it.)

So just as a quick recap, whenever the SQL service starts up, it attempts to register an SPN or service principal name in Active Directory.  An SPN is stored as an attribute on a user or computer account in Active Directory, depending on the security context in which the service is operating.  Even though the attribute name servicePrincipalName uses a singular tense, it's actually a multi-valued attribute that can and usually does contain many different SPNs for many different services.

So when you configure the SQL Server service (MSSQLSERVER) to run as Local System, the computer account for the computer that's running SQL needs the ability to write or update its own servicePrincipalName attribute on its computer account object in AD.  By default, computer accounts already have the permissions to write to their own servicePrincipalName attribute.  The name of the privilege is displayed "Validated write to service principal name:"

Write SPN permission

This is important that the computer only has the ability to write SPNs for itself, because it would be a major security concern for Active Directory accounts to be able to write SPNs on other accounts.

The validated write permission is restricted even further than just the regular ability to write to the servicePrincipalName attribute, as this causes the Directory Service Engine to reject updates that do not conform to the expected DNS FQDN and hostname format.  With just the basic write permissions, an account could theoretically write just any old invalid thing into the servicePrincipalName attribute, which is another security concern.  However, validated write to service principal name is only applicable to computer objects, not user objects.

So back to when the SQL Service starts up. When SQL is running as Local System, the computer account should have no problem registering an SPN for itself, like so:

Registered an SPN

However, most organizations run SQL services with a "service account," instead of Local System, and this is where things usually start to go pear-shaped:

SPN Registration Fail

You really should be using Managed Service Accounts for this, but the fact of the matter is that adoption of Managed Service Accounts is still very low and most organizations are still using what I would call "traditional" or "legacy" service accounts.  There are security advantages to using a service account to run SQL Server rather than using Local System. The main advantage being that if an attacker were to exploit SQL in some way, they could theoretically use that exploit to gain unlimited access to the entire system if SQL were running under the System account because the System account has unlimited access to the machine.  But the downside to using a regular user account as a "service account" is that regular user accounts do not, by default, have the permission to update SPNs, not even on themselves.

So let's fix that.

In other similar articles that you'll find on the web, you might see the author advising you to create a security group, putting all your service accounts into that security group, and then delegating the "Write servicePrincipalName" permission to that group for the entire domain.  I would call that practice suboptimal at best.  And by suboptimal, I mean terrible.  The reason it's terrible is because that gives every member of the "Service Accounts" group the ability to write service principal names for themselves and every other account in the domain, which is certainly a security hazard.

So try this as a better alternative:

First, make a "Legacy Service Accounts" OU or otherwise organize your service accounts into an OU.  (You know, right there next to the "Managed Service Accounts" container that you should be using, but I know you won't...)

Legacy Service Accounts OU

Next, open up ADSI Edit and connect to your default naming context.  (Nothing worse than searching for something in AD Users and Computers for 15 minutes before you realize that you can only find what you're looking for with ADSI Edit.)  Right-click on your "Legacy Service Accounts" OU and go to Properties.  Then go to the Security tab.  Click Advanced.  Click Add.  Click "Select a principal" and type in SELF and click OK. Leave the Type on "Allow" and change the Applies to: "Descendant User objects."  Scroll way down and check the box that says "Write servicePrincipalName".

Click OK a couple times to confirm and apply your changes.

Now what you've done is you've configured each account in that OU to inherit the SELF: write servicePrincipalName permission so that it is allowed to write SPNs on itself, but not on other accounts.  You can validate this by viewing the "Effective Access" of the SELF principal (in ADSI Edit - Not in ADUC!) of any given account in that OU.  You'll also notice that SQL Server starts logging success messages regarding SPN registration instead of failure.



Removing Stale Remote Desktop Licensing Service Connection Points From Active Directory

by Ryan 18. September 2014 13:09

I was doing some work with Remote Desktop Services today, and wanted to share a quick script I used that can keep your Remote Desktop license server service connection points tidy.

If you use Remote Desktop Services, formerly known as Terminal Services, then you have likely needed to deal with license servers and CALs, etc.  If you have a large and/or mature environment, chances are Remote Desktop License servers have come and gone in your environment.  The license servers get upgraded, or migrated, or retired, etc.  In any case, when you remove the Remote Desktop Licensing role from a server, it does not remove the corresponding "service connection point" from Active Directory.  Service Connection Points are simply objects in Active Directory that clients can use to auto-discover services throughout the domain, such as Exchange, SharePoint, Rights Management Services... and Remote Desktop Licensing servers.  When configuring a Remote Desktop Session Host, you might notice that when you go to point the session host to a license server, the list of available license servers is automatically populated from the SCP objects in Active Directory, and will contain stale/defunct license servers if the SCPs were never cleaned up.

So, how do we clean them up?  Simple:

$RDPSCPs = Get-ADObject -Properties * -Filter {(objectClass -EQ 'serviceConnectionPoint') -AND (Name -EQ 'TermServLicensing')}
Foreach ($SCP In $RDPSCPs)
{
    If ($(Get-ADComputer $SCP.serviceDNSName.Split('.')[0] -Properties LastLogonDate).LastLogonDate -LT (Get-Date).AddDays(-180))
    {
        Write-Warning "Deleting Remote Desktop Licensing Service Connection Point for the stale server: $($SCP.serviceDNSName.Split('.')[0])."
        # Add the -Confirm:$False parameter to the line below if you do not want to be prompted for confirmation.
        Remove-ADObject $SCP.ObjectGUID
    }
}

We simply find all the objects in Active Directory that are of the object class "serviceConnectionPoint" and with a name of "TermServLicensing."  That's important because there are many other types of SCPs other than just RDP License servers.  Then for each one of the SCPs found, we see if the server it refers to has not logged on to the domain in over 180 days.  If it has not, then we delete the SCP.

About Me

Ryan Ries
Texas, USA
Systems Engineer
ryan@myotherpcisacloud.com

I am a systems engineer with a focus on Microsoft tech, but I can run with pretty much any system that uses electricity.  I'm all about getting closer to the cutting edge of technology while using the right tool for the job.

This blog is about exploring IT and documenting the journey.


Blog Posts (or Vids) You Must Read (or See):

Pushing the Limits of Windows by Mark Russinovich
Mysteries of Windows Memory Management by Mark Russinovich
Accelerating Your IT Career by Ned Pyle
Post-Graduate AD Studies by Ned Pyle
MCM: Active Directory Series by PFE Platforms Team
Encodings And Character Sets by David C. Zentgraf
Active Directory Maximum Limits by Microsoft
How Kerberos Works in AD by Microsoft
How Active Directory Replication Topology Works by Microsoft
Hardcore Debugging by Andrew Richards
The NIST Definition of Cloud by NIST



MCITP: Enterprise Administrator

VCP5-DCV

Profile for Ryan Ries at Server Fault, Q&A for system administrators

LOPSA

GitHub: github.com/ryanries

 

I do not discuss my employers on this blog and all opinions expressed are mine and do not reflect the opinions of my employers.