Tuesday, September 29, 2020

 AD FS claims and cross-forest domain local group membership


Domain Local security groups are by defualt omitted from AD FS claims. In a two-way forest trust scenario where users reside in Forest A but the AD FS server and security groups reside in Forest B this may pose a problem if you want to pass along the security group membership (from Forest B) as a claim. 

To solve this, you can create a rule which translates SIDs to Name. The first rule will take the SIDs from the groupsid claim type and match it against the Forest B domain SID (to only query groups from the local domain). It will then use the SID and request the Name attribute as response. The group name will then be added to the claim flow. 

The next rule will take the groups the previous rule added to the flow and compare it to a defined prefix (in my example, group names beginning with "Prefix_"). If it finds a match, it will only issue the matching group names as a claim. 

If run on the AD FS server, the below code will automatically retrieve the AD FS service account name and domain SID and output the constructed claim rules. 



Add-Type -AssemblyName System.Security.Principal
$AdfsServiceAccount = (Get-WmiObject -Query "Select StartName From Win32_Service Where Name='adfssrv'").StartName
$NTAccount = [System.Security.Principal.NTAccount]::new($AdfsServiceAccount)
$NTAccountSecurityIdentifier = $NTAccount.Translate([System.Security.Principal.SecurityIdentifier])
$AccountDomainSid = $NTAccountSecurityIdentifier.AccountDomainSid

$RegEx = "^$($AccountDomainSid.Value)-[0-9]{4,}$"

$IssuanceTransformRules = @"
@RuleName = "Add group Name from SID as Group"
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value =~ "$RegEx"]
 => add(store = "Active Directory", types = ("http://schemas.xmlsoap.org/claims/Group"), query = "objectSid={0};Name;$($NTAccount.Value)", param = c.Value);

@RuleName = "Issue Prefix_ groups from Group"
c:[Type == "http://schemas.xmlsoap.org/claims/Group", Value =~ "^Prefix_"]
 => issue(claim = c);
"@

Write-Output $IssuanceTransformRules

Wednesday, November 26, 2014

Testing IP address validity in PowerShell

There may come a time when you want to test if an IP-address is valid or not. Here's a simple PowerShell function that will do just that and return true or false.

Function Test-IPAddress {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True, Position=0)]
        [String]$IPAddress
    )
   
    Begin {
    }

    Process {
        Return ($IPAddress -As [IPAddress]) -As [Bool]
    }

    End {
    }
}



Thursday, August 22, 2013

Install Windows Updates using a PowerShell script

or: How I stopped worrying and learned to live with Automatic Maintenance


As many of you may already know Windows Server 2012 does not play nice with WSUS/Windows Update. It will accept the download and notify or the download and schedule settings, but that's about where it ends. In other words, it will not honor the Windows Update schedule settings.

This is due to a new feature called Automatic Maintenance, which manages software updates in its own special kind of way and prevents us from forcing a reboot immediately after the updates have been installed.

If you are not yet sure how it works, this posts sums it up pretty well:
http://social.technet.microsoft.com/Forums/windowsserver/en-US/9714c384-ffed-4561-8349-32fd1dace9f9/server-2012-windows-update-group-policy


To work around this behavior and make sure that the updates get installed and the servers immediately rebooted I put together this PowerShell script. Together with the Windows Update policy to just download and notify it should at least ensure that the Windows Updates get installed during the proper server maintenance window.

I'm putting this script up here as-is and leave no guarantees that it's either perfect or will work for you. As always, watch for line breaks. If you find any bugs or have ideas on how to improve it, feel free to leave a comment.


[CmdletBinding()]
Param (
    [switch]$Reboot
)

$UpdateSession = New-Object -ComObject 'Microsoft.Update.Session'
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
$SearchResult = $UpdateSearcher.Search("IsInstalled=0 and IsHidden=0")
$RebootRequired = $False

$Install = New-Object -ComObject 'Microsoft.Update.UpdateColl'
for($i=0;$i -lt $SearchResult.Updates.Count;$i++) {
    $Update = $SearchResult.Updates.Item($i)
    Write-Verbose $Update.Title
    if($Update.InstallationBehavior.CanRequestUserInput) {
        Write-Verbose 'CanRequestUserInput True'
        Continue
    }
    if($Update.EulaAccepted -eq $False) {
        Write-Verbose 'Accepting EULA'
        $Update.AcceptEula()
    }
    if($Update.IsDownloaded) {
        Write-Verbose 'IsDownloaded'
        $Install.Add($Update) | Out-Null
        if($Update.InstallationBehavior.RebootBehavior -gt 0) {
            $RebootRequired = $True
        }
    } else {
        Write-Verbose 'NotDownloaded'
    }
}

if($Install.Count -eq 0) {
    Write-Verbose 'No updates to install'
    break
}

$UpdateInstaller = $UpdateSession.CreateUpdateInstaller()
$UpdateInstaller.Updates = $Install
$Results = $UpdateInstaller.Install()

$Date = (Get-Date).ToUniversalTime()
$DateUTC = Get-Date($Date) -Format 'yyyy-MM-dd HH:mm:ss'

$LastSuccessTime = $DateUTC
$LastError = $Results.HResult

Write-Verbose $Results

$RegPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install'
if(Test-Path -Path $RegPath) {
    $RegKey = Get-ItemProperty -Path $RegPath
    if($RegKey -match 'LastError') {
        Set-ItemProperty -Path $RegPath -Name 'LastError' -Value $LastError
    }
    if($RegKey -match 'LastSuccessTime') {
        Set-ItemProperty -Path $RegPath -Name 'LastSuccessTime' -Value $LastSuccessTime
    }
}

if($RebootRequired -or $Reboot) {
    Write-Verbose 'Rebooting in 10 seconds'
    Start-Sleep -Seconds 10
    Restart-Computer -Force
}


<#
    $Results.ResultCode:

    Return Value    Meaning
    0               Not Started
    1               In Progress
    2               Succeeded
    3               Succeeded With Errors
    4               Failed
    5               Aborted
#>



Saturday, July 30, 2011

Shadow Groups in Active Directory

Shadow Groups (SG) is an interesting topic. It’s not an actual type of group in the sense of security, distribution, global, domain local or universal, but rather a concept. A shadow group is a global security group which reflects the users found in a specific organizational unit (OU) in its group membership.

Shadow groups can be used for Fine Grained Password Policies, which can be applied to users (or inetOrgPerson objects) and global security groups only. Or if every user within a specific OU will require the same access to a resource, you can use shadow groups.

This means that you always want to keep the shadow groups up to date. If a new user is moved to the OU, it must also be added to the group. Likewise if a user is removed from the OU, it must also be removed from the group. There is no difference between a regular group and a shadow group in the sense that shadow groups bring any new or extra functionality when it comes to administration. These are just regular groups, but with a very specific purpose.

Keeping them up to date manually is really not an option, unless perhaps you know every single employee in the company personally and you are the only one managing AD. People aren’t machines, they make mistakes, it’s only human. Sooner or later, someone will forget to update the group when a user is moved between OUs.

This is when automation comes in handy. Anything that does not require human input should be automated.

Here we have a very simple criteria:
Users found in a specific OU should always be a member of a specific security group.
This does not require any human input or modification. We just need a way to compare the users in the OU with the users in the group and make any necessary changes.

Trust me when I say that there are many ways to do this. More than I will show you. I will limit this to the ds-tools and the PowerShell AD-cmdlets.

DS-Tools

The Quick and Dirty version:
dsquery user “<Organizational Unit distinguishedName>” –scope onelevel | dsmod group “<Shadow Group distinguishedName>” –chmbr

This will look for all users found in the specified OU, and limit the search to that OU only. Then it will clear the current group membership of the SG and add all users currently found in the OU.

The Clean and Clever batch file version:
Set OU=Organizational Unit distinguishedName (without quotes)
Set Group=Shadow Group distinguishedName (without quotes)

dsget group %Group% –members | find /v /i “%OU%” | dsmod group “%Group%” –rmmbr
dsquery * “%OU%” –filter “(&(sAMAccountType=805306368)(!memberOf=%Group%))” –scope onelevel | dsmod “%Group%” –addmbr


This will look at the group membership, pipe it to the find command, to find only the users where the OU’s distinguishedName is NOT present, and then pipe it to dsmod group to remove those users from the group. The next step is to look for all users in the specified OU that are NOT member of the Shadow Group already. It will then add any users found to the group.

PowerShell

Windows Server 2008 R2 with Active Directory cmdlets:
$OU=”Organizational Unit distinguishedName”
$Group=”Shadow Group distinguishedName”

Get-ADGroupMember –Identity $Group | Where-Object {$_.distinguishedName –NotMatch $OU} | ForEach-Object {Remove-ADPrincipalGroupMembership –Identity $_ –MemberOf $Group –Confirm:$false}
Get-ADUser –SearchBase $OU –SearchScope OneLevel –LDAPFilter “(!memberOf=$Group)” | ForEach-Object {Add-ADPrincipalGroupMembership –Identity $_ –MemberOf $Group}

This will do the same thing as the ds-tools clean and clever version, except it’s done in PowerShell with the AD cmdlets.

Once you’ve decided for what approach you want to take, you can easily create a scheduled task for this and ensure that the batch or PowerShell script runs at intervals that suits your organization. Just make sure that the user account the scheduled task runs under has got the proper privileges (such as log on as batch job and permission to update the Shadow Groups (write members) in Active Directory).

Thursday, July 28, 2011

The last VBScript you will ever need?

One of the first things people notice when writing their very first PowerShell scripts is that clicking the script file won’t run it, but instead open up Notepad. Which is nice, if you want to open up Notepad to read the script.
But I’m guessing that’s not why you clicked it.

There are of course several different ways to launch a PowerShell script. One is to run powershell.exe and specify the script as one of the arguments. This is useful if you want to run the script from a batch file, a shortcut, a scheduled task or wherever else you can specify a program to run.

I figured it would be neat to make the PowerShell script “clickable” by creating a shortcut to it and tell it to be run by powershell.exe. But doing this by hand is tedious work! (And as we all know, tedious work is the very reason you started scripting in the first place.)

Enter: VBScript

It’s easy to create a shortcut using VBScript, and it’s piece of cake to drag and drop a file onto another. –Which is something VBScript gladly accepts, taking the dropped file’s path as an argument to pass to the script. Knowing this, we can put two and two together, and write a script which allows you to drop a .ps1 file onto it, and then have it create a shortcut for us!

Now, this is a very straight forward script, which takes one file and creates a shortcut to it, adding the necessary parameters. It can be modified to allow you to drop several files onto it, or to run powershell.exe with additional parameters (such as setting execution policy).

Option Explicit

If WScript.Arguments.Count = 0 Then
    WScript.Echo "Drag and drop a .ps1 file onto the script."
    WScript.Quit
End If

Dim strPowerShellScriptFullPath : strPowerShellScriptFullPath = WScript.Arguments.Item(0)
Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")

If Not objFSO.FileExists(strPowerShellScriptFullPath) Then
    WScript.Quit
End If
If Not LCase(objFSO.GetExtensionName(strPowerShellScriptFullPath)) = "ps1" Then
    WScript.Quit
End If

Dim strPowerShellScriptPath : strPowerShellScriptPath = Left(strPowerShellScriptFullPath, InStrRev(strPowerShellScriptFullPath, "\"))
Dim strPowerShellScript : strPowerShellScript = Mid(strPowerShellScriptFullPath, InStrRev(strPowerShellScriptFullPath, "\"))
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim objShortCut : Set objShortCut = objShell.CreateShortcut(strPowerShellScriptPath & strPowerShellScript & " - Shortcut.lnk")

objShortCut.TargetPath = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
objShortCut.Arguments = "-NoExit -File """ & strPowerShellScriptFullPath & """"
objShortCut.WorkingDirectory = "%HOMEDRIVE%%HOMEPATH%"
objShortCut.IconLocation = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe, 0"
objShortCut.Description = "Run PowerShell script"
objShortCut.Save

I named my script “Create PowerShell Shortcut.vbs”.
And as always, there’s a very real risk of lines wrapping where they shouldn’t. If anything other than a .ps1 file is dropped onto it, nothing will happen (not even a message, in the above example).

When a ps1 file is dropped onto it, the script will create a shortcut to powershell.exe and assign the ps1 file path as an argument. The shortcut will be created in the same location as the ps1 file was dragged from. The VBScript and ps1 file does not have to be in the same location.


(It can of course also be run from the command line, as long as you pass a proper path, but that sort of defeats the purpose of it, now doesn’t it..)

Sunday, May 22, 2011

PowerShell, Active Directory and Expiring Passwords

Have you ever wanted to find out how many days are left until your (or someone else’s) Active Directory user account password expires? This (and just about everything else) can be done using Windows PowerShell.

Windows Server 2008 introduced a new Active Directory attribute called “msDS-UserPasswordExpiryTimeComputed”. This is a constructed attribute, which keeps track of when the password expires. Before Fine Grained Password Policies (FGPP) it used to be a simple matter of comparing the user’s pwdLastSet attribute with today’s date and subtracting it from the domain’s pwdMaxAge attribute.

I say used to be simple, because now you have to take FGPP into account. But as it turns out, it’s actually easier now, thanks to the new attribute.

Here comes the command. It looks a bit daunting at first, but broken down it’s actually quite simple.

PowerShell with the AD cmdlets:
(([datetime]::FromFileTime((Get-ADUser -Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed"))-(Get-Date)).Days

What we do here is begin with the Get-ADUser cmdlet followed by the user name, in this case AHultgren: Get-ADUser –Identity AHultgren
We also need to get the property containing the password expiration date. We do that by using the –Properties parameter and specify "msDS-UserPasswordExpiryTimeComputed".

Now if we wrap that entire command within parenthesis, we can return just the value of the attribute we need:
(Get-ADUser –Identity AHultgren –Properties "msDS-UserPasswordExpiryTimeComputed").”msDS-UserPasswordExpiryTimeComputed”

In my case the number 129538573151710728 is returned. This is not very helpful, so we need to translate the number to an actual date. We can do this by pre-fixing the command with [datetime]::FromFileTime() to convert it to a proper date:
[datetime]::FromFileTime((Get-ADUser -Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed")

This returns Wednesday, June 29, 2011 3:41:55 PM.

We could end it all here, since we now know the date the password will expire. But it can be interesting to know how many days are left. We do that by simply subtracting today’s date:
(([datetime]::FromFileTime((Get-ADUser –Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed"))-(Get-Date)).Days

Both the AD cmdlet and Get-Date must each be enclosed within parenthesis, and both commands must in turn also be enclosed within parenthesis, and then we can use “.Days” to just return the number of days. Instead of “.Days” you can pipe it all to Get-Member to show the available methods and properties you can use.

Running the final command looks like this:
PS C:\Windows\system32> (([datetime]::FromFileTime((Get-ADUser –Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed"))-(Get-Date)).Days
38

This means my password will expire in 38 days.


As an added bonus, this is how you turn a date into an Integer8 value:
(Get-Date(“2011-05-22”)).ToFileTime()
Which returns 129505176000000000.

Sunday, April 3, 2011

VBScript - WMI Ping

Sometimes it can come in handy to actually check, instead of just assume, that a destination is available before you let your script loose. One method you can use for this is ping. I recently wrote a log on script which pings a server before it attempts to map a network drive.

That script used a very simple, but yet effective, function that returned true or false based on the result. If ping failed (the function returned false), it would continue to try for 90 seconds before giving up. Here’s that little piece of code:

Function Ping(address)
    Ping = False
    Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
    Set objPing = objWMI.Get("Win32_PingStatus.Address='" & address & "'")
    If objPing.StatusCode = 0 Then
        Ping = True
    End If
End Function

WScript.Echo Ping("google.com")

To test this function, save it in a text file with the file extension .vbs, and run the file. It should return either –1 (true) or 0 (false) to the screen.

But perhaps you want a bit more information than that, since the above script was not made for human readability, but rather to be used by other functions within the script. 

So I put together two more functions for this purpose. PingStatusCode and PingStatus. The former will get the return code and the latter will translate the code to text, when it is passed to it.

Function PingStatusCode(address)
    Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
    Set objPing = objWMI.Get("Win32_PingStatus.Address='" & address & "'")
    PingStatusCode = objPing.StatusCode
End Function

Function PingStatus(PingStatusCode)
    Select Case Int(PingStatusCode)
        Case Int(0)
            PingStatus = "Success"
        Case Int(11001)
            PingStatus = "Buffer Too Small"
        Case Int(11002)
            PingStatus = "Destination Net Unreachable"
        Case Int(11003)
            PingStatus = "Destination Host Unreachable"
        Case Int(11004)
            PingStatus = "Destination Protocol Unreachable"
        Case Int(11005)
            PingStatus = "Destination Port Unreachable"
        Case Int(11006)
            PingStatus = "No Resources"
        Case Int(11007)
            PingStatus = "Bad Option"
        Case Int(11008)
            PingStatus = "Hardware Error"
        Case Int(11009)
            PingStatus = "Packet Too Big"
        Case Int(11010)
            PingStatus = "Request Timed Out"
        Case Int(11011)
            PingStatus = "Bad Request"
        Case Int(11012)
            PingStatus = "Bad Route"
        Case Int(11013)
            PingStatus = "TimeToLive Expired Transit"
        Case Int(11014)
            PingStatus = "TimeToLive Expired Reassembly"
        Case Int(11015)
            PingStatus = "Parameter Problem"
        Case Int(11016)
            PingStatus = "Source Quench"
        Case Int(11017)
            PingStatus = "Option Too Big"
        Case Int(11018)
            PingStatus = "Bad Destination"
        Case Int(11032)
            PingStatus = "Negotiating IPSEC"
        Case Int(11050)
            PingStatus = "General Failure"
        Case Else
            PingStatus = "Unknown Error"
    End Select
End Function

You can test the functions by adding these lines of code, outside the functions, before you run the script:

WScript.Echo PingStatusCode("google.com") & vbTab & PingStatus(PingStatusCode("google.com"))
WScript.Echo PingStatusCode("microsoft.com") & vbTab & PingStatus(PingStatusCode("microsoft.com"))

The script will first ping google.com and give you the return code and message, then it will ping microsoft.com and, again, give you the return code and message.

 

Here’s the MSDN reference for the Win32_PingStatus Class:
http://msdn.microsoft.com/en-us/library/aa394350(v=VS.85).aspx