I have powershell as my default shell, but I still end up using old cmd favourites like nslookup and repadmin. Now Ashley McGlone (GoateePFE) has created this handy reference for AD powershell, there is no excuse anymore.
This powershell function can be added to any script to allow logging to a text file. It allows each event to have a severity, and if you set a global debug level it will enable multi-level debug logging capability for the script.
It supports the usual get-help syntax that also contains a bunch of examples. The script picks up errors in $error so best to do an $error.clear() at the start of the script (a good idea as a matter of course).
<# .SYNOPSIS This function logs an input string to a text file prefixed by a date/time/severity. .DESCRIPTION Takes string input from a parameter or the pipeline and writes to a logfile, with appropriate date/time information. Allows specification of an (optional) severity. The function supports a global debug level in the script, so that it can be passed a per-event debug value and will decide whether to log the event based on the global debug setting. Also writes the output to the console for the host script to be easily used interactively. The script catches powershell errors and logs these too (eg. in a try/catch block). You must specify the logfile path $sLogFile in your script. If this is not set, a logfile will be created in this user's temp directory: %TEMP%\writelog.log. You may optionally specify an integer debug level $iDebug in your script, then specify for each event the debug level for which that event will be logged. .PARAMETER eventText The text to log .PARAMETER sev Severity. This appears in parentheses after the date/time. Convention suggests a single letter (although anything will be accepted) e.g. "E" = error, "W" = warning etc. Default is "I" (Informational). .PARAMETER noDate Writes eventText string without any date/time/severity preamble. e.g. for writing an initial title line of the logfile. .PARAMETER dbg Setting a value for this parameter will cause write-log to log the event ONLY if the value of the parameter is less than or equal to the global debug level $iDebug. e.g. if dbg is set to 2, write-log will only log this event if the global debug level is 2 or higher. Default for this is 0, i.e. log all messages. .EXAMPLE Write-Log "test event" writes to file: 01-01-2011 14:34:32 (I) : test event .EXAMPLE "test event 2" | Write-Log writes to file: 01-01-2011 14:34:32 (I) : test event 2 .EXAMPLE "test event error" | Write-Log -sev "E" writes to file: 01-01-2011 14:34:32 (E) : test event error .EXAMPLE "test event 4" | Write-Log -noDate writes to file: test event 4 .EXAMPLE write-log "event level 4" -dbg 4 writes to file: 01-01-2011 14:34:32 (I) : event level 4 if the global debug level has been set to 4 or greater, or does nothing if the global debug level has been set to between 0 and 3. If the global debug level has not been set at all then the event will always be logged. .EXAMPLE Write-Log -eventText "event 123","event 234" writes to file: 01-01-2011 14:34:32 (I) : event 123 01-01-2011 14:34:32 (I) : event 234 .NOTES AUTHOR: Dan Johnson (dan@djjconsulting.com) UPDATED VER REASON FOR UPDATE =================================== 05/03/2012 1.0 First Issue 10/10/2012 1.1 Added debug levels 06/12/2012 1.2 Improved pipeline handling .LINK http://dsablog.com .LINK http://djjconsulting.com #> Function Write-Log() { param( [Parameter(Position=0, Mandatory=$false, ValueFromPipeline=$true, HelpMessage="Please enter string to log")] [String[]]$eventText, [Parameter(Position=1, Mandatory=$false, ValueFromPipeline=$false)] [String]$sev = "I", [Parameter(Position=2, Mandatory=$false, ValueFromPipeline=$false)] [int]$dbg = 0, [switch]$noDate ) begin { if ($noDate) { $sPreamble = "" } else { $sNow = Get-Date -Format "dd-MM-yyyy HH:mm:ss" $sPreamble = "$sNow ($sev) : " } if ($sLogFile -eq $null) { $sLogFile = "$env:temp\writelog.log" } } process { foreach($item in $eventText) { if ($dbg -le $iDebug -or $iDebug -eq $null) { $sLogEntry = "$sPreamble$item" Write-Host $sLogEntry $sLogEntry | Out-File $sLogFile -append -encoding ASCII } } } end { if ($error.count -gt 0) { "$sNow (E) : ERROR" | Out-File $sLogFile -append -encoding ASCII "`t`t" | Out-File $sLogFile -append -encoding ASCII $error | Out-File $sLogFile -append -encoding ASCII "`t`t" | Out-File $sLogFile -append -encoding ASCII $error.clear()|out-null } } } # ******************************************************************************
In trying to count objects in a relatively large active directory (well over 1 million objects) Quest’s cmdlets are too slow, and I kept getting a weird memory leak with System.DirectoryServices. So why not try the old faithful adfind, by Joe Richards, just powershell-ized a little.
(((adfind.exe -c -b <searchroot> -f <filter> 2>&1)[-1]).split(" "))[0]
returns a single number for the count of the objects, and is pretty quick, plus saves about 6 lines of code.
e.g. all security groups:
# get object count $count = [int](((adfind.exe -c -b "dc=example,dc=com" -f "(&(objectclass=group)(|(samaccounttype=536870912)(samaccounttype=268435456)))" 2>&1)[-1]).split(" "))[0] # NB it outputs a string so you need the [int] cast to do any arithmetic on it
AskDS put up a post the other day which included a question about getting all DCs in a forest. I wrote a little powershell script a while back to do this, using System.DirectoryServices.ActiveDirectory. It doesn’t need ADWS so will run against 2003 DCs too š
Save the following as List-ADDomainControllers.ps1. It supports the standard powershell help format so from a PS prompt type:
get-help .\List-AdDomainControllers.ps1 -detailed
This will enumerate DCs in a domain or forest, and optionally allows you to enter a user/pass combo to use.
<# .SYNOPSIS Takes a domain or forest name and enumerates domain controllers. .DESCRIPTION Takes a domain or forest name in FQDN or X500 format and enumerates domain controllers. Will work with 2003 AD onwards as it does not require AD Web Services to run (uses System.DirectoryServices.ActiveDirectory namespace). .PARAMETER domain Domain or forest for which to enumerate domain controllers, in FQDN or X500 format. .PARAMETER user User credentials with which to bind (if not specified use the currently logged on user). .PARAMETER pass Password for account above. .PARAMETER forest If this is specified List-ADDomainControllers assumes the domain listed above is a forest root and enumerates DCs for all domains in the forest. .EXAMPLE C:\PS> .\List-ADDomainControllers.ps1 -domain corp.contoso.com Enumerate all domain controllers for corp.contoso.com domain .EXAMPLE C:\PS> .\List-ADDomainControllers.ps1 -domain contoso.com -user 'CONTOSO\admin' -pass 'Password123' Enumerate all domain controllers for contoso.com domain using the specified credentials .EXAMPLE C:\PS> .\List-ADDomainControllers.ps1 -domain contoso.com -forest Enumerate all domain controllers for all domains in the contoso.com forest .NOTES AUTHOR: Dan Johnson (dan@djjconsulting.com) UPDATED: 12/07/2012 .LINK http://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectory.aspx #> param ( [Parameter(Position=0,Mandatory=$true,HelpMessage="Please enter domain/forest name in FQDN or X500 format")] [string]$domain, [string]$user, [string]$pass, [switch]$forest ) # convert to FQDN if necessary $domain = $domain.toLower() if($domain.contains("=")) { $domain = $domain.trimstart("dc=").replace(",dc=",".") } if (!$forest) { if ($user -eq "") { $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$domain) } else { $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$domain,$user,$pass) } try { $collDcs = [System.DirectoryServices.ActiveDirectory.DomainController]::findAll($context) } catch { write-host "Logon failure, did you specify a valid username/password for this domain/forest?" -foregroundcolor red break } $collDcs | select name,sitename,domain | ft } else { if ($user -eq "") { $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("Forest",$domain) } else { $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("Forest",$domain,$user,$pass) } try { $oForest = [System.DirectoryServices.ActiveDirectory.Forest]::getForest($context) } catch { write-host "Logon failure, did you specify a valid username/password for this domain/forest?" -foregroundcolor red break } $collDomains = $oForest.domains foreach ($domain in $colldomains) { $collDcs = $domain.domaincontrollers $collDcs | select name,sitename,domain | ft } }
A couple of times while troubleshooting something else I have noticed that Kerberos still works even if the time skew is greater than 300 sec. On other occasions I have definitely seen it fail. As far as I knew this wasn’t supposed to happen, but I never got round to following it up as I was too busy with the original issue (and besides, it’s easy to fix; just set the time service to work properly!). Now finally here is an explanation from the AskDS guys as to why this is:
The semi-myth of Kerberos time skew
And the Technet article that I could never find but assumed must exist:
A client had a bespoke identity management app which cached AD group information so they could have autocomplete of group names on a self-service access request webpage. To keep their cache updated they were querying each DC in the domain (over 30) to get the highestCommittedUSN, then picking one at random to get groups with a newer USN. In this way they would never have to do a full sync, even if the DC they usually talked to failed (crazy way of doing it!). We needed a better solution, and I knew a way to do this using standard MS tools.
The standard System.DirectoryServices.DirectorySearcher can be configured to take a DirectorySynchronization control which saves a cookie of the directory state when it is run, so only deltas are shown when you run next time. The directory searcher can contain your usual LDAP string, in my case (objectClass=group).
I simply created a standard search using DirectorySearcher and then added a new DirectorySynchronization control. On first run, this has the effect of taking a baseline (all groups in this case) and dumping them into a list (this took say 20 secs for 70k groups). A cookie is generated which stores information about the state of the directory at this instant, and I get this with GetDirectorySynchronizationCookie() then write out to a text file (approx 1KB in size).
On subsequent runs, this cookie file is read using ResetDirectorySynchronizationCookie() and used to tell the searcher to only pickup changes since last time it was run then output these as a list. Depending on which attributes you specify to return you can get all sorts of changes here, but I specifically wanted add/delete/rename of groups. By specifying ExtendedDN and name as the to attributes this gave me name, DN, objectGUID and objectSID which was sufficient for the identity management app to update its cache.
The changes seen are add, delete and rename. Delete is obvious (see below). Whether you add or rename a group it appears the same but of course having the SID or GUID your app can see that this is either a new object or rename of an existing object.
This even works when the baseline is generated on one DC and the delta on another. Full code is below:
// a class to contain a single group result public class GroupInfo { public string Dn { get; set; } public string Name { get; set; } public string ObjectGuid { get; set; } public string ObjectSid { get; set; } } public static List<GroupInfo> GetGroups() { try { string sLdapServer = "LDAP://yourdc"; string sLdapFilter = "(objectClass=group)"; // file where cookie will be stored string sCookiePath = @"c:\temp\dirsync.dat"; // configure directory search DirectoryEntry dir = new DirectoryEntry(sLdapServer); DirectorySearcher searcher = new DirectorySearcher(dir); searcher.Filter = sLdapFilter; searcher.PropertiesToLoad.Add("name"); searcher.PropertiesToLoad.Add("distinguishedName"); searcher.SearchScope = SearchScope.Subtree; searcher.ExtendedDN = ExtendedDN.Standard; // create new dirsync object DirectorySynchronization sync = new DirectorySynchronization(); // check whether a cookie file exists and if so, set the dirsync to use it if (File.Exists(sCookiePath)) { byte[] byteCookie = File.ReadAllBytes(sCookiePath); sync.ResetDirectorySynchronizationCookie(byteCookie); } // assign dirsync object to the searcher object searcher.DirectorySynchronization = sync; // iterate over search results and enter into a list List<GroupInfo> liGroups = new List<GroupInfo>(); foreach (SearchResult result in searcher.FindAll()) { GroupInfo giGroup = new GroupInfo(); giGroup.Name = (string)result.Properties["name"][0]; string[] sExtendedDn = ((string)result.Properties["distinguishedName"][0]).Split(new Char[] {';'}); giGroup.Dn = sExtendedDn[2]; giGroup.ObjectGuid = sExtendedDn[0].Substring(6, 36); giGroup.ObjectSid = sExtendedDn[1].Substring(5, sExtendedDn[1].Length - 6); liGroups.Add(giGroup); } // write new cookie value to file File.WriteAllBytes(sCookiePath,sync.GetDirectorySynchronizationCookie()); return liGroups; } // catch LDAP errors catch (Exception exc) { List<GroupInfo> liGroups = new List<GroupInfo>(); GroupInfo giGroup = new GroupInfo(); giGroup.Name = String.Format("Error attempting to get data from LDAP: {0} ", exc.Message); liGroups.Add(giGroup); return liGroups; } }
I really like Quest’s ChangeAuditor (CAAD) tool. It allow such easy reporting and alerting on (and protection from) changes to your AD forests. There are a few shortcomings though, principally in how users can access the data, e.g.Ā it’s not possible to easily ‘scope’ the console so that non-admin users can see things of interest. It is however a relatively easy to read SQL schema so that we can do custom things with it.
One of the things I like to do for all new clients is to setup an ASP.NET website to allow admin staff and users to see information about account lockouts, such as which machine locked it out and when. With the gradual creep of non-enterprise standard devices such as iPads, iPhones etc there are more pressures put on helpdesk staff as there are more places for things to go wrong. iPads/iPhones are particularly bad at dealing with credentials and as soon as you change your domain pw that wireless profile you setup on your iPad is going to keep locking out your domain account!
CAAD to the rescue! Simply open your CAAD SQL box and create a stored procedure as follows:
USE [ChangeAuditor] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE PROCEDURE [dbo].[usp_zAccountLockouts] AS BEGIN SET NOCOUNT ON; SELECT TOP 5000 AET.TimeDetected, AET.UserAddress, AET.UserName, ISNULL(TSR.ServerName, TWP.MachineName) AS ServerName, ISNULL(TDN.DomainName, TWP.WorkgroupName) AS DomainName, AET.TimeZoneOffset FROM Audit.Event AS AET INNER JOIN [Event].Class AS ECS ON AET.EventClassID = ECS.EventClassID INNER JOIN [Event].[Action] AS EAN ON AET.ActionID = EAN.ActionID LEFT OUTER JOIN Topology.[Server] AS TSR ON AET.AgentID = TSR.ServerID LEFT OUTER JOIN Topology.Domain AS TDN ON TSR.DomainID = TDN.DomainID LEFT OUTER JOIN Topology.Workgroup AS TWP ON AET.AgentID = TWP.MachineID WHERE AET.TimeDetected >= DATEADD(hour, -2, GETDATE()) and EventClassName = 'User Account Locked' ORDER BY TimeDetected DESC END
This will show all account lockouts in the last 2 hours for the domains/forests that your CAAD installation manages. You can then run the stored procedure from your ASP.NET pageĀ and give all users access to the website, no special permissions required, so they can see from which computer their account was locked out, and at what time.
And the iPads? Well typically if a lockout is caused by an iDevice then it is from the wifi profile. Typically the wifiĀ Ā controller will be a Cisco WCS and report itself as CISCO in this tool.
So now instruct the users to read the knowledgebase article on the intranet about lockouts, and check this webpage. It’s amazing how the password unlock/reset calls drop away once this is implemented!
Server 2012 Release Candidate landed yesterday. I decide to install on my portable lab environment,Ā where I already have a Win8 AD setup.
VMware workstation doesn’t know 2012/win8 yet so you need to choose ‘install OS later’, then selecting 2008 R2 as the target OSĀ seems to work fine.
So MS have chosen a nice new blue for server 2012, I like to think there is a focus group somewhere at Redmond that spent ages choosing the particular shadeĀ š I also like the new monochrome logo styling, cf win8/ie10.
The install is really quick, not like the good old days. Once installed and networking/computername configured it’s time to dcpromo. This is all done through the role/feature system now.
Select Active Directory Domain Services, and DNS (assuming you want this to be a DNS server), then a few next/nexts and Install.
Once installed, next step is to actually PromoteĀ this server to a domain controller.
The config steps are broadly similar to previous versions (and the same as win8 obviously). I am adding to an existing Win8 Beta AD domain.
The install when adding to a domain/forest suggests it will need to run the adprep functions. This is being added to a Win8 forest which in theory is the same, but perhaps they have had to do some schema updates to change ‘8’ into ‘2012’ and fix whatever else they found wrong with the beta.
I ignored the messages about DNS delegation (there isn’t one in my lab) and NT4 cryptography, then went ahead and let it configure. It reboots and you are done. I checked and it disagrees with the win8beta DC as to what the functional level is called:
But a quick check of msDS-Behavior-Version shows it to be the same level, 5.
So all went fine, my win8preview and samba clients can connect with no problems and the domain didn’t spontaneously combust, all replication etc checks out fine. Having said that, I haven’t managed to get the win8 beta version to crash yet, lots of dogfooding by Microsoft means betas aren’t what they used to be!
So one of the firewall guys asked me about some drops on port 464 (kpasswd) for a new client location we setup in Paris. I was under the impression MS included kpasswd for UNIX interoperability, as I was pretty sure that MS operating systems didn’t use it. No issues had been reported changing passwords, even though many new users were at the site and would have been forced to change. Some new Windows 7 machines had been installed at the site, some of the first at our organisation (yes we are behind!).
I couldn’t get hold of any users onsite so I got wireshark on a test W7 machine talking to a test 2008 functional level domain and took a look at the traces when changing password using ctrl+alt+del ‘change password’ option. Sure enough, Win7 uses KPASSWD protocol to change passwords. From the trace below (filter “dns || ntlmssp || kerberos || samr”) you can see the client sends AS_REQ to the authentication server and obtains a ticket for the kadmin/changepw SPN (another type of ticket the AS issues besides the TGT):
On receiving the response it sends the KPASSWD packet to the DC and receives the response (you can take my word for it that it was ‘success’), then issues new requests based on the new password:
I tested various scenarios and in fact the same situation occurs for Win7 clients talking to 2003 (functional level and all 2003 DCs), whether this is a client and server in same forest, or in different domains both forest trust and external trust. (This was the situation with the site we setup, since that domain was at 2003 level.)
The situation when using an XP client is entirely different (I didn’t have a Vista client to test – who does in a commercial environment?). XP always uses SAMR (RPC-over-SMB, on 445/tcp), whether it is talking to a 2008 or 2003 based domain, either in the same forest or a trusted (external or forest) domain:
On my test Win7 box I blocked port 464 tcp/udp and sure enough it still allowed the password change, but reverted to SAMR like XP (in my trace I guess you don’t see the KPASSWD request since Comodo firewall blocked it before Wireshark saw it):
So, given that Win7 ALWAYS defaults to KPASSWD (for any 2003 or 2008 domain, trusted or single domain) why isn’t _kpasswd port 464 in the Microsoft list of domain ports that we supply to our firewall teams?
EDIT: looks like they added it finally, better late than never…
Microsoft have released a beta version of the Script Explorer for Powershell:
http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=29101
Looks pretty good, as long as I can get some custom repository locations configured (which is apparently possible). This should save my team some duplication of scripts and snippets, and also improve our plagiarising re-use of IP š