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

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 {
        [Int]$PotentialMS14068s = 0
        Write-Output "Fetching Security event log from $DC."
            $Events = Get-EventLog -LogName Security -InstanceId 4624 -ComputerName $DC -ErrorAction Stop
            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
        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

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

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.

Powershell Code That Literally Writes Itself - Automaception

I amused myself with Powershell today and thought I'd share. I might have also named today's post, "Write-Host does have a use after all!"

Today, I had the task of synchronizing the country attributes of thousands of users from a non-Microsoft LDAP server into multiple Active Directories.  This non-Microsoft LDAP server stored the country attribute ("c" in LDAP parlance) of each user as an ISO 3166 Alpha-3 three-letter abbreviation.  I wanted to convert that into the ISO 3166 alpha-2 two-letter notation before I imported it into Active Directory, as well as fill out the rest of the country-related attributes at the same time.

As most AD administrators know, when you want to programmatically set a user's country, you have to make the change in three different places if you want to be thorough.  You have to change co (Text-Country,) c (Country-Name,) and countryCode (Country-Code.)  Those three fields are a free-form text entry, an ISO-3166 A2 or A3 abbreviation, and a numeric value, respectively.

So the first thing I do is track down the ISO 3166 list.  It looks like this:

AALAND ISLANDS                                  AX      ALA     248
AFGHANISTAN                                     AF      AFG     004
ALBANIA                                         AL      ALB     008
ALGERIA                                         DZ      DZA     012

And on and on... for 240 countries.

I was thinking I'd want a Switch statement in my script... the idea is that I Switch($x) where $x is the three-letter country abbreviation that came from the LDAP server. Visions flashed through my mind of me staying up all night writing this monotonous switch block with 240 cases in it.  And then I thought, "Hey, Powershell is the most powerful automation framework there is. Surely I can use it to automate the automation!"  And so I set out to have my Powershell script literally write itself.

First, save that ISO 3166 country code list to a text file. That the list is already in a fixed-width format is going to make this extremely simple.

$Countries = Get-Content C:\Users\Ryan\Desktop\3166.txt

Write-Host "Switch (`$Transaction.NewValue.ToUpper().Trim())" #E.g. 'USA' or  'BEL'
Write-Host "{"
Foreach ($Line In $Countries)
    [String]$CountryCode = $Line[64] + $Line[65] + $Line[66]
    [String]$ThreeLetter = $Line[56] + $Line[57] + $Line[58]
    [String]$TwoLetter   = $Line[48] + $Line[49]
    [String]$FreeForm    = $Line.Substring(0, 46).Trim().Replace("'", $Null)

    Write-Host "    '$ThreeLetter'"
    Write-Host "    {"
    Write-Host "        Set-ADUser -Identity `$Transaction.ObjectGUID -Replace @{ countryCode = $CountryCode } -ErrorAction Stop"
    Write-Host "        Set-ADUser -Identity `$Transaction.ObjectGUID -Replace @{ co = '$FreeForm' } -ErrorAction Stop"
    Write-Host "        Set-ADUser -Identity `$Transaction.ObjectGUID -Replace @{ c = '$TwoLetter' } -ErrorAction Stop"
    Write-Host "    }"
Write-Host "    Default { Write-Error `"`$(`$Transaction.NewValue) was not recognized as a country.`" }"
Write-Host "}"

When I ran the above script, it printed out all the code for my massive switch block, so that all I had to do was copy the text out of the Powershell window, and paste it into the middle of the script I was working on.  It came out looking like this:

Switch ($Transaction.NewValue.ToUpper().Trim()) #E.g. 'USA' or  'BEL'
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ countryCode = 248 } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ co = 'AALAND ISLANDS' } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ c = 'AX' } -ErrorAction Stop
    # ... 238 more countries ...
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ countryCode = 716 } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ co = 'ZIMBABWE' } -ErrorAction Stop
        Set-ADUser -Identity $Transaction.ObjectGUID -Replace @{ c = 'ZW' } -ErrorAction Stop
    Default { Write-Error "$($Transaction.NewValue) was not recognized as a country." }

And there we have it.  Now I don't have any employees from Burkina Faso, but it's nice to know that I'd be able to classify them if I did.  Now with all the time I saved myself from having to type all that out by hand, I figured I'd write a blog entry about it.

Be the Master of the LastLogonTimestamp Attribute with S4U2Self

I've written a little bit about the LastLogonTimestamp/LastLogonDate attribute here, and of course there is AskDS's notorious article on the subject here, but today I'm going to give you a handy little tip that I don't think I have mentioned before.

If you're an Active Directory administrator, chances are you're interested or have been interested in knowing if a given account is "stale," meaning that the account's owner has not logged in to the domain in some time.  (Keep in mind that an account could be either a user or a computer as it relates to Active Directory.)  You, like many sysadmins, might have some script or automated process that checks for stale accounts using a command-line approach, such as:

dsquery user -inactive 10

or Powershell's extremely flexible:

Get-ADUser -Filter * -Properties LastLogonDate
| ? { $_.Enabled -AND $_.LastLogonDate -LT (Get-Date).AddDays(-90) }

And then you take action on those inactive accounts, such as moving them to an "Inactive Users" OU, or disabling their accounts, or sending a reminder email to the account holder reminding them that they have an account in this domain, etc.

It might be handy for you to "artificially" update the lastLogonTimeStamp of another user though.  Maybe you know that this user is on vacation and you don't want their user account to get trashed by the "garbage collector" for being inactive.  According to the documentation, lastLogonTimeStamp is only editable by the system, so forget about directly modifying the attribute the way that you would other LDAP attributes.  And of course "LastLogonDate" is not a real attribute at all - merely a calculated attribute that Powershell gives you to be helpful by converting lastLogonTimestamp into a friendly .NET DateTime object.

The S4U2Self (Service for User to Self) Kerberos extension can help us here.

Just right click on any object such as an OU in Active Directory a folder in a file share, go to its Properties, then the Security tab.  Click the Advanced button.  Now go to the Effective Permissions tab.  Click the Select... button, and choose the user whose lastLogonTimestamp you want to update.  We are going to calculate the effective permissions of this inactive user:

By doing this, you are invoking the S4U2Self Kerberos extension, whereby the system will "go through the motions of Kerberos authentication and obtain a logon for the client, but without providing the client's credentials. Thus, you're not authenticating the client in this case, only making the rounds to collect the group security identifiers (SIDs) for the client."[1]

And just like that, you have updated the "Last Logon Time" on another user's behalf, without that user having to actually log on themselves.