Sunday, January 26, 2014

Automating IP addresses with TMG

In the past few months I've had the "opportunity" (note the quotes, maybe I'll write a post about all the troubles I've run into) to deal with some Office 365 integration projects.

When integrating one or more cloud services with Office 365 or Office 365 with on-prem Exchange, it is typical to limit the IP addresses that are aloud to communicate between services. This is an important layer on the security model and obviously reduces the attack surface. Microsoft maintains lists of IP addresses for the various services here:

http://onlinehelp.microsoft.com/en-us/office365-enterprises/hh373144.aspx

That page links to other pages with more specific information about various other Microsoft cloud services such as the link below for Exchange online.

http://help.outlook.com/en-us/140/gg263350.aspx for example.

At the time of this writing, you'll notice that the lists are inconsistent between pages. >:-(

Microsoft on two separate Premier Support Calls has told me that those lists are not kept up to date. As a result of this, I've experienced Journaling black-holes and Hybrid-configuration black-holes. In other words: I have discovered new IP addresses from Microsoft datacenters that were not published on their lists and so connections were being denied. In the case of the Journaling black-hole, Journaled messages were lost. This is bad, especially for litigation-hold situations. But I digress...

(NB: I have been told that there is an RSS feed that contains the various IP address lists which could be consumed via PowerShell or other automated task, but I've not been able to find it)

Anyway - all of this whining is tangential to the actual point of this post. The point of this post is that in one configuration I was working on recently, we were using a Threat Management Gateway (which was a cancelled product and replaced with UAG, which is a cancelled product and presumably to be partially replaced with WAP (? - conflicting information from Microsoft's team on this too)).

To configure a TMG rule to only accept traffic from specific hosts or subnets, those subnets must be entered as an Subnet Object (or some other type of range object supported by TMG). This isn't so hard right? Wrong. The user interface for TMG, when trying to bulk-add addresses, is terrible.  I imagine anyone who is reading this blog post already has some experience so I won't complain about it too much...

Here is how I handled the situation - some of it was more manual than I wanted - but at the time I was in a time crunch and haven't refined my process. That's mostly because I don't have a TMG in my lab. Anyway, here are steps I'd taken:


  1. Collect IP addresses from the web site referenced above - in my case, I put them in a Notepad++ window
  2. Convert the IP addresses to this format:
    "Unique Name","IP Address", "Subnet Mask"
    for example one address is "65.54.62.0/25", convert that to
    Office IP Subnet 1,65.54.62.0,255.255.255.128
    I used regex's to perform this operation.
  3. Save that file as a CSV (make sure there are headers - I saved mine as 'SubnetList.csv')
Step 4 is a little more complicated - so here's what you can do from an elevated PowerShell:

$tmg = new-object -comobject "FPC.Root" -strict
$tmgarray  = $tmg.arrays | select-object -first 1
import-csv .\SubnetList.csv | % { $tmgarray.ruleelements.subnets.add($_.Name,$_.Network,$_.Subnet)}
$tmgarray.save()

Step 5
This will add all of the subnets as Objects within the TMG and then they can be easily applied to the rule. You could also add the subnets programatically to the rule(s) but I haven't tested it. I'm not sure if I'd recommend that outside of a lab environment first though since you don't get to easily and visually review the configuration when you do it that way. 

So that's it... while this post seems long, I assure you that this mechanism is much, much faster than performing this operation manually.

Good luck!

Friday, August 30, 2013

LDAP and SAN Certificates

Hey everyone - I ran across an interesting problem with certificates and services. The problem was that I needed to see which certificate is being presented to the client on a non-HTTPS service; specifically - binding to LDAP over SSL (LDAPS). So the question is, how can you be sure what certificate is being presented?

Windows, so far as I've been able to find, does not offer any native help in this regard. Luckily there is a solution, but first let me give you a slightly longer description of the scenario so you can appreciate what we're talking about a little bit better.

An environment exists with some number of Domain Controllers, let's say ten. Within this environment, third party applications (think Java apps, Linux systems, etc) need to bind to LDAP to enumerate groups or validate authentication or whatever. These systems, however, can only have a single (or at best, two) LDAP hosts configured. What do you do? Pick two DCs? Round-Robin DNS?

Well, in my case, a load-balancer had been configured to direct LDAP(S) traffic to the various domain controllers and a DNS record configured for that load balancer.

This works great except that when using a load balancer, a generic name (like LDAPS.domain.com) must be used since it is unknown which server will be connected to. This is a problem for SSL.

If I connect to LDAPS.domain.com and am presented with a certificate whose subject reads "dc01.domain.com" then my SSL bind will fail. The solution to that is to create certificates with Subject Alternate Names[1] such that the "LDAPS.domain.com" name exists in addition to each of the certificates used by each of the Domain Controllers.

Starting with Server 2008, you can bind certificates to specific services (though this was NOT true with Server 2003). So, after binding the SAN cert to AD DS, we want to verify which certificate we're seeing when binding to LDAPS. If you want to actually see the certificate details - here are the steps to view the presented cert:
  1. Install OpenSSL[2]
  2. Open a PowerShell session or Cmd prompt
  3. From the OpenSSL\bin directory, issue the following command:
  4. openssl s_client -connect server:port | openssl x509 -noout -text
(you may have to hit the space bar or enter key for the data to be displayed)

In my case, I wanted to verify that I was seeing the certificate with all the Subject Alternate Names. This proved to be exactly what I needed to see the certificate presented to a binding client. Sadly - I cannot give you screen shots since the data is private and I don't have a lab with all of the certificate infrastructure built. Feel free to comment though, if you need a hand with the commands.

G. Samuel Hays

Monday, August 26, 2013

HomeDirectory name inconsistency and LDAP Query weirdness

Recently I was doing some work where I had to archive some home directories of people who were no longer associated with the company. A typical configuration looks something like this:


User:samAccountName  -> \\Server\Home\%username%

Such that the samAccountName property has the same name as the directory in whatever home share is being used.

What I ran up against, though, was that over the years - through marriages or divorces or other personal reasons, sometimes people would request that their username be changed. The company I was working with sometimes accommodates these requests. My job was to identify if any of the home folders were associated with accounts that had different names.

For example - consider this timeline:

1. User bjones is created with home directory \\Server\Home\bjones
2. bjones gets married and her account name is changed to bsmith

Now user bsmith has \\server\home\bjones for her home directory. This typically isn't a problem technically but administratively it's a pain.  The question is, how do we quickly find out which folder is associated with which Active Directory user?

First, I'll tell you what did not work - and this is surprising!  Within Powershell, I browsed to the Home Folder share and issued the following command:

dir | select fullname | % { Get-ADUser -LDAPFilter "(homeDirectory=$($_.fullname))" }

(Logic: the folder names correspond with the samAccountName so I could use that for the ADUser and for the homeDirectory LDAP query)

This command worked but only on some accounts. In some cases, even though the homeDirectory matched exactly, no result was returned from Active Directory. Why?   I don't know. Independent testing of this has not yielded any solutions, though it has yielded the same results.  So - this is a problem I'm going to have to re-approach.

Still - the problem had to be solved so I came up with another way to get that information. That solution was this:

Get-ADUser -Filter * -Properties homedirectory,enabled -ResultSetSize $null | ? {$_.homeDirectory -ne $null -and $_.homeDirectory -notmatch $_.samAccountName }| select na

e,samaccountname, homedirectory, enabled, @{l="path_Exists";e={test-path $_.homedirectory}} | ft 

The logic here as as follows:
1. Get a list of all AD users and include the homedirectory (and just for fun) the enabled property
2. Use "where-object" (?) to filter out users without a homeDirectory and require that their samAccountName is not found in the path of their homeDirectory
3. Select the properties I want and build a new property for whether or not the folder name defined in homeDirectory actually exists

With this information I am able to quickly see users who have an inconsistently named Home Directory folder, if their account is enabled, if the specified path exists, and of course, their full name.  Enough information to make some decisions on.


Friday, August 23, 2013

Net User and Reset Password Delegation

Hey Folks - I ran into an interesting issue recently where a user who has been given the right to change the password for another domain user, could not do so.  Let me give you a bit of background.

I have a user (let's call him "Account Owner") who manages another account that is used to bind to Active Directory (over LDAPS). Let's call that account "Bind User".  So, Account Owner has the rights to change the password for Bind User.

I told the Account Owner that he could issue the following command to change the bind_user account password:

Net User Bind_User * /domain

I informed him that this command would prompt him for a new password and he could manage the account that way (so he didn't need to install any additional management tools). I told him that this should work from any computer to which he is currently logged on.  The problem, as it turns out, was that I was wrong.  But why?

To answer that question, I configured one of my labs so that I can reproduce the issue. I'll walk you through the issue and solution(s).

The configuration


I created a new user called "bind_user" in an OU called "Delegated Rights Users".



In the "IT Dept" OU, I have a user called "Account_Owner".

The first step is to adjust the bind_user such that Account_Owner can reset the password. So I added those permissions:

Allow CREATION\account_owner          SPECIAL ACCESS for pwdLastSet
                                      WRITE PROPERTY
                                      READ PROPERTY
Allow CREATION\account_owner          Change Password
Allow CREATION\account_owner          Reset Password

This means that, outside of resetting the password (which includes writing the pwdLastSet property), Account_Owner cannot make any adjustments. (This should have been the end of our configuration, or so I thought.)

Testing The Config

As I mentioned earlier, I had directed Account Owner to use the "Net.exe" command to reset the password. But he got the following error.


"Access is Denied" is a surprising result given the permission that Account_Owner has been granted. This raises a question - what other attributes is the "Net.exe" command trying to alter?

To find out, I created another user account called "bind_user_full" and granted Full Control to the "Account_Owner" account.  Next, I took a snapshot of the attributes prior to issuing the command.

Note: I used "get-aduser bind_user -properties * | fl * -force" with a transcript running to capture the attributes into a text file.

Once I'd issued the Net.exe command, I took another snapshot of the AD attributes so I could make a comparison and see what has been modified.

Here are the attribute that have changed on the bind_user_full account after I reset the password with Net.exe:

accountExpires (original value):9223372036854775807
accountExpires (new value):     0

logonHours (original value): NULL
logonHours (new value):      {255, 255, 255, ... }  an array of values

Modified (original value): 8/23/2013 10:09:57 AM
Modified (new value):      8/23/2013 10:18:43 AM

modifyTimeStamp (original value): 8/23/2013 10:09:57 AM
modifyTimeStamp (new value):      8/23/2013 10:18:43 AM

passwordLastSet (original value): 8/23/2013 10:09:38 AM
passwordLastSet (new value):      8/23/2013 10:18:43 AM
* pwdLastSet matches this value

uSNChanged (original value):  32873
uSNChanged (new value):       32876

whenChanged (original value): 8/23/2013 10:09:57 AM
whenChanged (new value):      8/23/2013 10:18:43 AM

As we can see, quite a few attribute were changed after issuing the "Net.exe" command to reset the password. To identify which attributes were changed by the Net command and which were changed by Active Directory, I performed the same comparison, except using the ADSI provider in PowerShell to change the password of the original account*.

*Note: Using PowerShell to change the password instead of the Net command is how I stumbled upon this behavior in the first place.


With the password having been changed through the ADSI provider, I performed the same comparison as before.

Modified (original value): 8/23/2013 9:41:28 AM
Modified (new value):      8/23/2013 10:35:07 AM

modifyTimeStamp (original value): 8/23/2013 9:41:28 AM
modifyTimeStamp (new value):      8/23/2013 10:35:07 AM

passwordLastSet (original value): 8/23/2013 8:57:09 AM
passwordLastSet (new value):      8/23/2013 10:35:07 AM
* pwdLastSet matches this value

uSNChanged (original value):  32847
uSNChanged (new value):       32880

whenChanged (original value): 8/23/2013 9:41:28 AM
whenChanged (new value):      8/23/2013 10:35:07 AM


This behavior is much more consistent with what I expected.  Apparently the Net command makes a lot of assumptions about what you mean when you give it a command.  (Another example is "net user username /active:yes /domain"  both unlocks the account AND enables it, if it is disabled. - that can be a bad thing).

Conclusion


So it seems that the Net.exe command is doing quite a bit more behind that scenes that I'd expected. I could continue to investigate, identify all of the attributes that it wants to change, and grant them but I find that approach distasteful.

Instead, I directed Account Owner to use the PowerShell method to change the password until I get an opportunity to put a decent script together for him.

G. Samuel Hays

Wednesday, August 21, 2013

Get-Help vs Help - the paging/pager inconsistency

When I'm teaching PowerShell to folks, I often like to use the full name of the cmdlets - get-help instead of 'help', get-childitem instead of 'dir'. You get the idea.

I do this to help drive home the verb-noun syntax and because I can't necessarily expect everyone to know the shortcuts that I tend to use.

Well, the problem that I'm about to describe hasn't come up too much - but its annoying and I finally decided to take a minute and figure out what's going on.

The Problem (Powershell 2.0 or 3.0)

In powershell, if I issue the command:

get-help <cmdlet> -full

then I get a wall of text that is not paged (where paging is that "-- More  --" at the bottom of the screen that allows me to read a page at a time.

To be honest, I'd never really spent more than a second thinking about this annoyance because it seemed so trivial, but since I'm in the midst of putting together a curriculum for a powershell class, it got a little more important.

I had basically made the assumption that help (and man) were simply aliases to get-help. This assumption was wrong.  Have a look:


PS C:\> get-alias -Definition get-help
Get-Alias : This command cannot find a matching alias because alias with definition 'get-help' do n
ot exist.
At line:1 char:10
+ get-alias <<<<  -Definition get-help
    + CategoryInfo          : ObjectNotFound: (get-help:String) [Get-Alias], ItemNotFoundException
    + FullyQualifiedErrorId : ItemNotFoundException,Microsoft.PowerShell.Commands.GetAliasCommand

PS C:\>

That's surprising, no aliases at all for get-help? That makes me wonder what "man" is aliased to. Well as it turns out, "man" is pointing to "help".

PS C:\> get-alias man

CommandType     Name                                      Definition
-----------     ----                                      ----------
Alias           man                                       help

Alright, so what is "help" if it isn't an alias but does the exact same thing as get-help? And what's the deal with "man" aliasing to it?! Let's find out.

PS C:\> Get-Command help

CommandType     Name                                      Definition
-----------     ----                                      ----------
Function        help                                      ...

Ah ha! As we can see (highlighted for dramatic effect) - "help" is not an alias. It is a function! Let's have a look at this function.

PS C:\> cd function:
PS Function:\> dir help

CommandType     Name                                      Definition
-----------     ----                                      ----------
Function        help                                      ...


PS Function:\> Get-Content help

<#
.FORWARDHELPTARGETNAME Get-Help
.FORWARDHELPCATEGORY Cmdlet
#>
[CmdletBinding(DefaultParameterSetName='AllUsersView')]
param(
    [Parameter(Position=0, ValueFromPipelineByPropertyName=$true)]
    [System.String]
    ${Name},

    [System.String]
    ${Path},

    [System.String[]]
    ${Category},

    [System.String[]]
    ${Component},

    [System.String[]]
    ${Functionality},

    [System.String[]]
    ${Role},

    [Parameter(ParameterSetName='DetailedView')]
    [Switch]
    ${Detailed},

    [Parameter(ParameterSetName='AllUsersView')]
    [Switch]
    ${Full},

    [Parameter(ParameterSetName='Examples')]
    [Switch]
    ${Examples},

    [Parameter(ParameterSetName='Parameters')]
    [System.String]
    ${Parameter},

    [Switch]
    ${Online})
$outputEncoding=[System.Console]::OutputEncoding

      Get-Help @PSBoundParameters | more

PS Function:\>

And there we have it! The function (again, highlighted for dramatic effect) simply runs the get-help cmdlet put pipes the output to the "more" command. 

Well - that explains the different behavior, and that's good to know.

Interestingly - the "more" command is also simply a function. Output below for the sake of completeness:

PS Function:\> type more
param([string[]]$paths)
$OutputEncoding = [System.Console]::OutputEncoding

if($paths)
{
    foreach ($file in $paths)
    {
        Get-Content $file | more.com
    }
}
else
{
    $input | more.com
}

Interesting stuff.  I hope if any of you guys wondered about this, you now have the answer and can sleep better at night. :)

Friday, September 9, 2011

Windows Last Logon problem and solution

As someone who has been involved in Network Administration (in Microsoft Land) since Windows NT 4.0, I find it surprising that is still so difficult to get simple (yet important) information such as "When was the last time Joe User logged in?".

One would think that, with the fourth edition of Active Directory in production (Windows Server 2008 R2), a tool or set of tools would have been issued with Windows to provide those answers.  Well, because they don't, I've decided to go ahead and write one.  (Yes, I know that there are probably others out there to be downloaded or purchased but... you know, I don't care.)

All that is required: PowerShell 2.0 (and a functioning Active Directory).

Defining the Problem:
Active Directory stores numerous properties on objects in the Directory. Some of these properties are replicated amongst the Domain Controllers and some are not. Unfortunately, for some reason, one of the design decisions was to not replicate the "LastLogon" property for Computer and User accounts. So, for example, if I log in with username "sam" on DomainController01 then the LastLogon property is updated on that Domain Controller.  However, DomainController02 has no record of when I've logged on and will never know, unless I happen to authenticate against it at some point.

This is not a problem if an organization has only one Domain Controller (as is often the case in small shops), but in the event of a shop with two or more, it can be quite misleading. For example, many administrators will use the "net user" command to see when the last time a user logged onto the network was:


Unfortunately, the time-stamp highlighted in red will only be accurate if the user being checked on happened to authenticate to the same Domain Controller that I authenticated to. This is because the "net.exe" command will inquire against the currently set LogonServer (which can be viewed by typing "set logonserver" from a command line).

Therefore: the "net user" command, with regard to "Last Logon", cannot be trusted in environments with more than one Domain Controller.


Defining the Solution:


So, what is required to actually get the /true/ last logon?  There are two solutions:

1). Crawl through all of the event logs on all of the servers. I don't particularly care for this option.
2). Basically one has to query each Domain Controller and compare the answers, with the newest one being authoritative. See the flow chart below.

Note: Some folks would argue for the "LastLogonTimestamp" property to be queried because it is replicated between DCs. This doesn't work though, because that is designed to find old accounts that are generally inactive, not for immediate (and exactly accurate) results. (It can be as much as 14 days incorrect - see: The Technet Blog article.)


Diagram of solution 2:

This will ensure that we get the latest value for LastLogon.

To accomplish the above task, I've written a little Powershell "Advanced Function". It gathers the names of all of the Domain Controllers in the organization, then queries each of them a la the flow chart above. In accepts Pipeline input in the form of "username" or "sAMAccountName".

Example usages:


The screen shot above, I give a couple of examples. The first one, I simply pass in a samAccountName (or "username").  The second, I pass in an array of usernames, which the script will process in order. Finally, in the 3rd example, I use the Quest ActiveRoles ADManagement snapin (free download) to pipe the output of "Get-QADUser" to the script. This is to illustrate that the get-LastUserLogon accepts pipeline input.

The script is as follows:
#============= COPY BELOW THIS LINE =============

<#
    .Synopsis
        get-LastUserLogon will return the last logon date of a user from a Microsoft
        Windows domain environment.
    
    .Description
        get-LastLogon will query all of the domain controllers and find the most
        recent LastLogon time. This function is required because Active Directory
        does not replicate that information between Domain Controllers.
   
    .Parameter user
        The User parameter is a list of sAMAccountNames (common usernames) to query for
        last logon time.
        
    .Inputs
        This will receive a property called "User" from the Pipeline and use that
        as basis for querying Active Directory.
    
    .Outputs
        Produces an object that has the following properties:
            [String] User
            [DateTime] LastLogon
            [String] LastLogonDC
#>


#requires -version 2.0


[CmdletBinding()]
param(
    [Parameter(
        ValueFromPipelineByPropertyName = $True,
        Position = 0,
        Mandatory = $True,
        HelpMessage="Username (sAMAccountName) or Usernames to process.")
    ]
    [Alias("samAccountName")]
    [String[]]
    $user
)


begin {
    $DCs = ([System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()).DomainControllers
    $domDN = (New-Object System.DirectoryServices.DirectoryEntry).distinguishedName
}


process {
    $user | ForEach-Object {
        $currentUser = $_
        $lastLogon = [DateTime]0
        $lastLogonDC = ""
        $lastLogonObject = New-Object Object
        $DCs | ForEach-Object {
            $currentDC = $_.name
            $strFilter = "(&(objectCategory=Person)(samAccountName=$currentUser))"
            $objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$currentDC/$domDN")
            $objSearcher = New-Object System.DirectoryServices.DirectorySearcher
            $objSearcher.SearchRoot = $objDomain
            $objSearcher.PageSize = 1000
            $objSearcher.Filter = $strFilter
            $objSearcher.SearchScope = "Subtree"
            $colResults = $objSearcher.FindAll()            
            
            $colResults | foreach-object {
                try {
                    $currentLastLogonVal = [datetime]::FromFileTime([int64]::Parse($_.Properties.lastlogon))
               
                    if($currentLastLogonVal -gt $lastLogon) {
                        $lastLogon = $currentLastLogonVal
                        $lastLogonDC = $currentDC
                    }
                } catch {


                }
            }
        }
        
        [String]$lastLogonString = ""
        if($lastLogon -ne [DateTime]0) {
            $lastLogonString = $lastLogon
        } else {
            $lastLogonString = "Never"
        }
        $lastLogonObject | Add-Member noteproperty "LastLogon" -Value $lastLogonString
        $lastLogonObject | Add-Member noteproperty "Username" -value $currentUser
        $lastLogonObject | Add-Member noteproperty "LastLogonDC" -value $lastLogonDC
        
        Write-Output $lastLogonObject
    }
}


end {


}
#============= END COPY ABOVE THIS LINE =============


This script is fully functional and I use it in production. There isn't a huge amount of error handling, but feel free to add it if you are so inclined. :)

I hope someone finds this valuable.

gsamuelhays