Archive

Posts Tagged ‘Powershell’

Powershell and Unknown User SIDs

October 9th, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

Once again, my apologies for my lack of posting..

Anyway…

An interesting thing came up at work the other day when one of my fellow administrators asked me if I could resolve an unknown SID he was seeing in some logs to see what the heck it belonged to.

Since I had been telling him that Powershell could do ANYTHING (slight exaggeration, I know)… that it should be able to do this.

Well, it certainly is an interesting notion.

I know that I have run into this in the past where you have file system ACLs set and there are a bunch of SIDs sitting in there that nobody seems to know who they belong to.

While it isn’t THAT important to resolve them since the user account is most likely no longer around, it IS an interesting thought exercise.

After perusing the web looking for others who have done something similar, I feel I had enough to throw something together..

Basically when an Active Directory object (like a user) is “deleted”, it is really just Tombstoned for a period of time and is moved to the hidden container “Deleted Objects” and then removed after like 90 days or so.

Here is my solution to “finding” those objects.

First you will have to know what Domain you want to look at for this object AND you have know the SID you are looking for.

function Resolve-DeletedUserSID($Domain, $UserSID)
{
	## This is kind of a mashup of a few different scripts I found online in some forums.
	## Unfortunately I don't remember who did them.  If it was you, point me to your post and I will
	## give you the credit for your piece.
	## 
	## Returns User information for deleted account with the specified SID and User Domain
 
	$DomainRoot = "LDAP://" + $Domain.trim()
	$DomainDN = ([adsi] ( $DomainRoot )).DistinguishedName
	$adspath = "LDAP://" + $DomainDN
	$root = [system.directoryservices.Directoryentry] $Adspath
	$root.psbase.AuthenticationType = [system.directoryservices.authenticationtypes]::Fastbind
	## We will be looking in the "Deleted Objects" container which is normally hidden, etc.
	## You will need to execute this with an account that has DomainAdmin rights to the domain you are
	## querying.
	$root.psbase.path = "LDAP://cn=Deleted Objects," + $DomainDN
	$search = [system.directoryservices.directorysearcher] $root
	$search.filter = "(&(isDeleted=TRUE)(!(objectClass=computer))(objectclass=user))"
	$search.Tombstone = $true
 
	# If you have more than 1000 users, you must NOT define SizeLimit (we haven't)
	# and PageSize must be less than the default value (of 1000). 
	# I found this a bit strange...but as long as we understand it, I guess it is okay
	$Search.PageSize = 500
 
	# Only look in the top level of the Deleted Objects container.
	$search.SearchScope = [system.directoryservices.searchscope]::OneLevel
	$result = $search.FindAll()
 
	# If the SID isn't found, you will get nothing returned.
	$result | Select-Object @{ Name = "Name" ; Expression = { $_.Properties.Item("Name")[0].split("`n")[0] } }, `
	@{ Name = "SAMAccountName" ; Expression = { $_.Properties.Item("SAMAccountName") } }, `
	@{ Name = "SID" ; Expression = { New-Object System.Security.Principal.SecurityIdentifier($_.Properties.Item("ObjectSID")[0], 0) } }, `
	@{ Name = "WhenChanged" ; Expression = { $_.Properties.Item("WhenChanged") } } `
	| where-Object { $_.SID -ieq $UserSid.Trim() }
}

As always…

Happy Scripting!!!! and let me know if you have questions or problems.

– Mark

Powershell – Recursive Group Membership

August 16th, 2009 Mark A. Weaver 18 comments
Rating 3.50 out of 5

Well, I am back for yet another Powershell script. This is one that I found pretty useful actually.
As one of the people really pushing automation in my group at work, I was tasked with getting a list of all users in the “Domain Admins” group for all domains in our Active Directory forest.

One of the challenges in doing this is that you may have a bunch of nested groups and we needed to dump users from all nested groups, etc.  I do realize that there are probably tools and what-not that would this for me, but what fun is that and why spend the bucks if you can script it out.  I like this approach, too, because I can force the output to be whatever I want and in whichever format is best for what I am trying to accomplish.

From my days in college as a computer science kinda guy, I figured we could use recursion to help walk us through all nested groups.

So for those of you unfamilar with this idea of recursion, I will summarize:
It is basically a function that calls itself until a certain condition is met. At this point the function exits. I know all you CS types may take exception to such a simplified definition, so please google for it or hit up Wikipedia for more detailed info on recursion.

How can this possible help us in our quest to enumeration group memberships?  Well let’s break it down a little.

  1. I start with a group I care about. Let’s say it is “Domain Admins” for domain “office1.contoso.com”.
  2. I have a function (“get-groupmembers”) that I use to enumerate the members of this group and do something with them (output, write to file, etc)
  3. As I am enumerating them, I find a member that is of type “group” called something like “Corp Admins”
  4. I now call my function (“get-groupmembers”) with this nested group (“Corp Admins”) to enumerate the members
  5. As I am enumerating them, I find ANOTHER group called “Help Desk On-Call” inside of “Corp Admins”
  6. I can now call my funciton (“get-groupmembers”) ANOTHER time and keep going until I only have users and have walked all of the nested groups

I know this may sound a little weird “that I am calling myself” over and over again, but it is actually pretty efficient.

Let’s jump into some code now.

For starters, I need to have some functions that take a Fully Qualified Domain Name (for an Active Directory Domain) and convert it into an LDAP-ish format.  For example, I needed “office1.contoso.com” to be transformed into “DC=office1,DC=contoso,DC=com”.  I know this isn’t rocket-surgery, but I just threw some stuff together for it.

function Convert-DNStoDN ([string]$DNSName)
{
   #  Create an array of each item in the string separated by "."
   $DNSArray = $DNSName.Split(".")
  # Let's go through our new array and do something with each item
   for ($x = 0; $x -lt $DNSArray.Length ; $x++)
      {
        #I don't want a comma after my last item, so check to see if I am on my last one and set
        # $Separator equal to nothing.
        # Remember that we need to go to Length-1 because arrays are "0 based indexes"
         if ($x -eq ($DNSArray.Length - 1)){$Separator = ""}else{$Separator =","}
         [string]$DN += "DC=" + $DNSArray[$x] + $Separator
      }
   return $DN
}

We will also need to be able to split the FQDN of the DOMAIN out from the DN of a group or user. So, I have something like “CN=Me,OU=User1,DC=office1,DC=contoso,DC=com” and want to get the FQDN of this domain. For this example this would output “office1.contoso.com”.

function Convert-DNtoDNS ([string]$DN)
{
    $DNArray = $DN.Split(",")
     # Let's go through our new array and do something with each item
   for ($x = 0; $x -lt $DNArray.Length ; $x++)
      {
        #I don't want a period after my last item, so check to see if I am on my last one and set
        # $Separator equal to nothing.
        # Remember that we need to go to Length-1 because arrays are "0 based indexes"
         if ($x -eq ($DNArray.Length - 1)){$Separator = ""}else{$Separator ="."}
        # Now we have to see if we look like "DC=". If it does, we will
        # start to construct our DNS name.
        if ($DNArray[$x].Split("=")[0] -ilike "DC")
          {
               # Let's grab the "contoso" side of the "DC=contoso"
              [string]$DNS += $DNArray[$x].Split("=")[1] + $Separator
           }
      }
   return $DNS
}

Now that we have those little “cameo” functions, we will move on to the more of the meat-and-potatoes of the script.
The next function will be to actually enumerate a group in Active Directory without using the Quest Tools for Active Directory (if you don’t have those yet, you need to them).

We are actually going to use some .NET calls to get the directory objects. My colleague Mike Hays actually did a lot of this part of the code.

function get-groupmember([string]$domain, [string]$groupName)
{
   # I have passed in the FQDN and Groupname I am interested in
   # I just need to convert my FQDN into an LDAP style name using my previous function
   $DN = convert-DNStoDN($Domain)
   $domainLDAPUrl = "LDAP://" + $DN
   # Setup my directory connection using .NET call
   $ent = [System.DirectoryServices.DirectoryEntry] ( $domainLDAPUrl )
 
   # Define my "searcher" object to query the directory
   $srch = [System.DirectoryServices.DirectorySearcher] ( $ent )
 
   # Setup my search criteria.. looking for all Groups with CN=GroupName
   $groupNameFilter = "(&(objectClass=group)(CN=" + $groupName + "))"
   $srch.Filter = $groupNameFilter
 
   # Now go execute my query to and put the results in $coll
   $coll = [System.DirectoryServices.SearchResultCollection]      $srch.FindAll()
 
   foreach ($rs in $coll)
     {
       # Now get a collection of properties for that object
       $resultPropColl = [System.DirectoryServices.ResultPropertyCollection] $rs.Properties
 
       # Cycle through all group members
       foreach ($memberColl in $resultPropColl["member"])
         {
           # Build my membership array
           [array]$gpMemberEntry += [System.DirectoryServices.DirectoryEntry] ( "LDAP://" + $memberColl )
          }
   }
  # Send back my group members.
  return $gpMemberEntry
}

Okay.. now that we have THAT setup let’s talk about the next bits of code.
This is where we will have our recursive function “Get-AllMembers”. In it, you will a call to itself. One of the biggest concerns is that you can end up in an unending or infinite cycle. I don’t really do any checking in this little scripty-do-dad, so that may be something for later.

function get-allmembers($objectName, $OF, $GN)
{
    # Split out my domain name  (should be FQDN) and the group name
    $domainName = $objectname.split("\")[0]
    $ObjectName = $objectname.split("\")[1]
 
    $members = get-groupmember "$DomainName" "$ObjectName"
    if ($members -ne $NULL)
     {
        foreach ($member in $members)
         {
            #  Grab the domain DNS name out of the object DN
            $ObjDomain = convert-DNtoDNS $Member.DistinguishedName
            if ($member.objectclass -contains "group")
              {
                 #If my group member is, itself, a group We get to do some recursion
                  $out = $objDomain + "\" + $member.name
                  Write-Host $out
                  # Call myself with the nested group name
                  get-allmembers -ObjectName $out -OF $of -GN $GN
               }
            else
               {
                  # If I get back a user, then see if the user is disabled or not
                  $userAndDomain = $objDomain + "\" + $member.name
                  # The  UserAccountControl property contains several "flags"
                  # that we can interrogate.  By doing a Binary AND we are seeing if the 2nd flag is set.
                  [bool]$accountIsDisabled = [int]$member.userAccountControl.ToSTring() -band 2
 
                  # Setup our output (I am choosing to construct a comma-delimited type of output
                  $OutText = "'" + $GN + "','" + $objDomain + "','" + $Member.Samaccountname + "','" + $Member.displayName + "','" + $member.distinguishedName + "','" + $objectname + "','" + $accountIsDisabled + "'"
 
                  Out-File -FilePath $OF -inputobject $Outtext -append -Encoding "ASCII"
 
                  Write-Host $OutText
                }
         }
    }
    else
     {
        Write-Host "No Members or no Group:" $ObjectName "in Domain:" $DomainName -Foreground RED
     }
}
 
###########################
## Main
###########################
$GroupName = "Domain Admins"
$Today = Get-Date -format "yyyyMMddhh"
$OutputFolder = "C:\Temp\"
 
if ((Test-Path $outputFolder) -eq $False)
 {
    New-Item -Path $OutputFolder -Type Directory > $NULL
  }
 
# Grab my forest info
$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
 
#Setup my output file
$OutHeader = "'Group','UserDomain','SAMAccount','DisplayName','DN','MemberofGroup','IsDisabled'"
 
# Define output file name
$of = $OutputFolder + $Today +"_"+ $GroupName + "-AuditReport.txt"
 
Out-File -FilePath $OF -inputobject $OutHeader -Encoding "ASCII"
 
# Cycle through all the child domains in the forest root to query for Group.
foreach ($domain in $forest.Domains)
 {
    $FullGroupName = $domain.name + "\" + $GroupName
    get-allmembers -ObjectName $FullGroupName -OF $of -GN $FullGroupName
}

Just take all of the script blocks from above and paste them into your script. I am trying to keep these posts a bit shorter, so you may see upcoming posts broken out into parts.

Well, I think I am done here with this one. Please let me know if you have questions, concerns, or comments.

Please keep in mind that this script will attempt to enumerate the Group in ALL child domains in your current AD Forest. If you have a large Forest with lots of child domains…..this could take a while.

I will be happy to help out with requested changes if they seem like they would be beneficial overall, but I am also a STRONG advocate of doing-it-yourself.
Every bit of Powershell and scripting I have learned by grabbing it and going with it.

Anyway, as always…thanks for stopping by and happy scripting!!!

– Mark

Binary Search and Powershell

July 22nd, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

Time for another Powershell Post.
So, what is a binary search and why should you care about it?

Well, it provides us a mechanism to very quickly search through an ordered list of “things” (like event log entries). It is especially useful when looking through LARGE amounts of data.

I know, I know.. There are all sorts of ways to look through/filter/search event logs and everyone has probably written about this already, but I guess I feel compelled to put in my 2 cents.

We ran into the need to be able to pull event log entries from a specific period of time on several servers.
There aren’t really any “native” Powershell methods to query Eventlogs from REMOTE servers (unless you are on CTP and hitting Vista/2K8 systems).

So a little info about a binary search:

Lets say we have a list of things as follows:

Index

Name

DOB

0

Adam 01/20/80

1

Cheryl 09/16/77

2

Frank 04/01/82

3

Ivan 10/26/70

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

And we are wanting to find which index “Suzy” is in the list.

One way would be to start at the top of the list and see if the Name matches. On a short list this may make sense since it is really only 5 compares before we find her.

But if I was looking for “Suzy” in a list the size of a phone book this can take a long time and a lot of horsepower to find her and may take 300,000 compares or more.

A binary search algorithm helps us by breaking the list down by using something we already know about the list: that it is sorted by name.

With a single compare, though, we can cut the list of viable options in half.

All we need to do is take the index of our smallest value [0] and we add it to the index of our largest element [6] and divide by 2, we get the index of our middle element [3]. Now lets compare what we are looking for (“Suzy”) to the value of element [3].

LowerBound Index = 0
UpperBound Index = 6
Mid Index = (6+0)\2 = 3

Lets take a look at this:

Index

Name

DOB

0

Adam 01/20/80

1

Cheryl 09/16/77

2

Frank 04/01/82

3

Ivan 10/26/70

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

Obviously “Suzy” is greater than our value at index [3] “Ivan”. Since we know this, we can throw out all indexes that are [3] and lower.

Our list of viable options now becomes this:

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

Let’s repeat this exercise on our new list:

Lowest index = 4

Largest index = 6

Middle index = (6+4) /2 = 5

 

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

 So now we compare what we are looking for “Suzy” to our element [5] (“Tim”). Well, “Suzy is less than “Tim”, so that means we can throw out indexes that are [5] and higher.

 

4

Suzy 04/10/90

 Let’s go one more time…. oh wait.. .there is only one thing left….it’s “Suzy”. We have found her.

 

There ARE .NET methods (of [System.Array]) that enable us to do binary searches of Arrays for Strings. The only problem is that it will be looking for EXACT matches for the string.

 

This poses a problem for us if we are looking for something that is a little less exact.

 

Mark’s Modified Binary Search

Okay, leave it to me to mess with a good thing.

My problem is that I want to grab all events from the System event log that occurred between 8pm and 9pm last night.

This is a little more “fuzzy” than exact, so let me show you what I did….heh

 I know you all are getting a little antsy for some code, but we will get there soon.. Stay with me.

function Get-DatedLogEntries([string]$ServerName, [string]$EventLogName, [datetime]$OldestTime, [datetime]$NewestTime)
{
 
	#Grabbing my Eventlog Entries
	$EventLog = New-Object System.Diagnostics.EventLog($EventlogName)
	$EventLog.MachineName = $ServerName
	$Entries = $Eventlog.Entries

There are some cmdlets designed to return eventlog entries, but they are only really effective if you are querying the local server or if you are leveraging the “new” Powershell Remoting, but you have to be running against Vista or Server 2008.

Fortunately, there is a way to grab remote event log entries via .NET (as shown in the code above)

When done, $Entries will contain all of the events in the logfile sorted by Time.

Next, we will setup our “bound”ing values for our array of entries and the times we are looking for.

#Defining my starting boundaries of my array
$Ubound = $Entries.count - 1
$Lbound = 0
$Mid = 0
 
#Setting up my dates
$StartTime = $OldestTime
$EndTime = $NewestTime

Now we are ready for the real work. Because I want to grab a range of events, I will run through the binary search two times. The first time will tell me what the UpperBound of my list is, and the second will tell me what the LowerBound of my list is as array indexes.

Before we start checking anything, we can make sure our search isn’t for naught.

Many systems have event logs that can roll pretty quickly, so we can just compare the oldest event to our start time. If the oldest event is newer than my start time, then my logs have rolled and I won’t have any data.

if ($Entries[0].TimeGenerated -lt $StartTime)
{
while (($Ubound - $Lbound) -gt 1)
{
   $Mid = [int] ( ($Ubound + $Lbound) / 2 ) #Calculate my midpoint
  #Compare my midpoint to my StartTime
  if ($Entries[$Mid].TimeGenerated -lt $StartTime)
     {
  #If my midpoint is less than my Start time, then throw out all events
  #below and including my Midpoint
        $LBound = $Mid + 1
     }
   elseif ($Entries[$Mid].TimeGenerated -gt $StartTime)
     {
     #If my midpoint is greater than my Start time, then throw out all events
      #above and including my Midpoint
      $Ubound = $Mid-1
     }
    else
     {
     #If my midpoint is equal to my Start time, then I got lucky and found my time.
     #I just realized that I may need to do something else with this. May tackle that
     # later though...
      $Ubound = $Mid
      $LBound = $Mid
     }
  }
 # Now I know that the array index of our oldest item is here
 $Oldest = $LBound

Now we will repeat the process to find out LowerBound. I won’t document it too much since it is nearly identical code to above. If you have questions, please let me know.

$Ubound = $Entries.count - 1
$Lbound = 0
$Mid = 0
 
while (($Ubound - $Lbound) -gt 1)
   {
   $Mid = [int] ( ($Ubound + $Lbound) / 2 )
   if ($Entries[$Mid].TimeGenerated -lt $EndTime)
     {
      $LBound = $Mid + 1
     }
    elseif ($Entries[$Mid].TimeGenerated -gt $EndTime)
     {
      $Ubound = $Mid-1
     }
    else
    {
     $Ubound = $Mid
     $LBound = $Mid
    }
 }
$Newest = $LBound

So now we have the LowerBound index for the range I am looking for.
Now to finish it up…

# Sometimes we can end up with endpoints that don't meet our time range
# We fix that by going through each side and adjusting them until they
# are correct
 while ($Entries[$Newest].TimeGenerated -gt $Endtime)
  {
   $Newest--
  }
 
 while ($Entries[$Oldest].TimeGenerated -lt $StartTime)
  {
   $Oldest++
  }	
 
if (($Entries[$Newest].TimeGenerated -lt $StartTime) -and ($Entries[$Oldest].TimeGenerated -gt $EndTime))
 {
    #Insert Code here if you want to do something when
    #no events found during the period requested..
    $EntriesByDate = $NULL
 }
else
 {
    #Create a new array and assign it the $Entries indexes ranging from our 'oldest' to our 'newest'
    $EntriesByDate = $Entries[$Oldest..$Newest]
  }
}
else
{
	#Insert Code here if you want to do something when
	# Logs have rolled... and none were in the specified range.
	$EntriesByDate = $NULL
 }
return $EntriesByDate
}

I hope I was able to articulate this effectively. As always, let me know if you have questions, comments, or suggestions.
Thanks for Reading and happy shelling.
So, that is basically it. Here is the code in one big block:

## Get-DatedLogEntries Function
## Written by: Mark A. Weaver
## Website: www.vmweaver.com
## Version: 1.0
## Date: 7/23/2009
## Purpose: This Function will get event log entries from the specified server using currently logged in
##          credentials and return an array of Events that occurred between the 2 times.
##          Not much error checking or validation is done, so you please edit to your liking.
##
##        Input:
##				-ServerName "ServerName"
##				-EventLogName "EventLogName"
##          -OldestTime [DateTime]OldestTime
##				-NewestTime [Datetime]NewestTime
##
##        Output:
##				Array of Event log entries or Null if none found
#############################
## Updates:
##
##
##
######################################################################
######################################################################
 
function Get-DatedLogEntries([string]$ServerName, [string]$EventLogName, [datetime]$OldestTime, [datetime]$NewestTime)
{
 
	#Grabbing my Eventlog Entries
	$EventLog = New-Object System.Diagnostics.EventLog($EventlogName)
	$EventLog.MachineName = $ServerName
	$Entries = $Eventlog.Entries
 
	#Defining my starting boundaries of my array
	$Ubound = $Entries.count - 1
	$Lbound = 0
	$Mid = 0
 
	#Setting up my dates
	$StartTime = $OldestTime
	$EndTime = $NewestTime 
 
	if ($Entries[0].TimeGenerated -lt $StartTime)
	{
		while (($Ubound - $Lbound) -gt 1)
		{
			$Mid = [int] ( ($Ubound + $Lbound) / 2 ) #Calculate my midpoint
			#Compare my midpoint to my StartTime
			if ($Entries[$Mid].TimeGenerated -lt $StartTime)
			{
				#If my midpoint is less than my Start time, then throw out all events
				#below and including my Midpoint
				$LBound = $Mid + 1
			}
			elseif ($Entries[$Mid].TimeGenerated -gt $StartTime)
			{
				#If my midpoint is greater than my Start time, then throw out all events
				#above and including my Midpoint
				$Ubound = $Mid-1
			}
			else
			{
				#If my midpoint is equal to my Start time, then I got lucky and found my time.
				#I just realized that I may need to do something else with this. May tackle that
				# later though...
				$Ubound = $Mid
				$LBound = $Mid
			}
		}
 
		# Now I know that the array index of our oldest item is here
		$Oldest = $LBound 
 
		$Ubound = $Entries.count - 1
		$Lbound = 0
		$Mid = 0
 
		while (($Ubound - $Lbound) -gt 1)
		{
			$Mid = [int] ( ($Ubound + $Lbound) / 2 )
			if ($Entries[$Mid].TimeGenerated -lt $EndTime)
			{
				$LBound = $Mid + 1
			}
			elseif ($Entries[$Mid].TimeGenerated -gt $EndTime)
			{
				$Ubound = $Mid-1
			}
			else
			{
				$Ubound = $Mid
				$LBound = $Mid
			}
		}
		$Newest = $LBound		
 
		while ($Entries[$Newest].TimeGenerated -gt $Endtime)
		{
			$Newest--
		}
 
		while ($Entries[$Oldest].TimeGenerated -lt $StartTime)
		{
			$Oldest++
		}	
 
		if (($Entries[$Newest].TimeGenerated -lt $StartTime) -and ($Entries[$Oldest].TimeGenerated -gt $EndTime))
		{
			#No events found during the period requested..
			$EntriesByDate = $NULL
		}
		else
		{
			#Create a new array and assign it the $Entries indexes ranging from our 'oldest' to our 'newest'
			$EntriesByDate = $Entries[$Oldest..$Newest]
		}
	}
	else
	{
		# Logs have rolled... and none were in the specified range.
		$EntriesByDate = $NULL
	}
	return $EntriesByDate
}