FIX: 'Cannot Write Pam Settings' when Joining a Windows Domain in SuSE 10.3

Filed under: Active Directory, Linux, Networking — Written by Chrissy on Saturday, January 19th, 2008 @ 12:29 pm

Today I attacked my 2008 technical to do list and setup a subversion server for backups/source control. It was actually pretty darn easy in SUSE 10.3. After I got it going, I wondered if I could have it automatically authenticate against my HOME domain. So, using SuSE's menu driven interface YaST, I easily added my Linux machine to my Windows domain.

Initially, YaST wasn't able to find or join the domain. This happens sometimes in Windows clients too when:
1. In TCP/IP, the DNS settings are pointed to servers outside of the domain
2. The fully qualified domain name (ex. corp.windomain.com) is not given when joining the domain
3. The FQDN is not listed as a DNS search suffix

After adjuting /etc/resolv.conf to reflect my fully qualified domain name, YaST made it surprisingly easy to find and join the domain. But right as it was finishing up, it ran into the error "cannot write pam settings." I looked around the web and saw about 2 other people had the same problem but no solution was offered. After poking around, I noticed that "pam-smb" was not installed. Generally, SuSE will automatically detect when rpms need to be added but in this case it didn't.

In order to get it all working, I added pam-smb, samba-winbind and krb5-client then I easily plugged into my Windows 2003 domain. Years ago, I tried to do something similar and it seemed to work but I was never able to login via SSH. I'm pretty sure I didn't prefix the domain (in proper case, at that) when attempting to login. Knowing that, I was successfully able to login to my Linux machine using a Windows domain login this time around.

login as: HOME\testuser
Using keyboard-interactive authentication.
Password: *****************
Creating directory '/home/HOME/testuser'.
Creating directory '/home/HOME/testuser/public_html'.
Creating directory '/home/HOME/testuser/bin'.
Creating directory '/home/HOME/testuser/Documents'.
Have a lot of fun...
HOME\testuser@subversion:~>

Awesome! This is much easier than doing user mapping with NIS.

Powershell: Working with Passwords

Filed under: Active Directory, PowerShell, Security — Written by Chrissy on Thursday, July 26th, 2007 @ 9:30 pm

When creating a new Active Directory user from the command line in PowerShell, you will likely find yourself using Read-Hosts's asSecureString switch when entering the password.


$password = Read-Host "Enter password" -AsSecureString

Next, you'll probably look around the Internets for a few hours or so trying to figure out how to change the password of the newly created user. You will soon discover that the user creation process in PowerShell 1.0 isn't very straightfoward and it even requires a specific order for proper account creation. First, you create the account, then you set some basic properties, next you call SetInfo(), and finally you invoke setPassword using the follwing syntax:


$newUser.psbase.Invoke("SetPassword",$password)

Now you may find yourself with the following exception: Exception calling "Invoke" with "2" argument(s): "Exception has been thrown by the target of an invocation." Originally, this post mentioned using toString() to address the problem but PowerShell team member Lee Holmes wrote to let me know that the password was changed literally to System.Security.SecureString. He also said that "there is no really easy way to convert a secure string to plain text - on purpose. Since a SecureString is supposed to prevent plain text from littering your computer's memory, converting it to plain text defeats the purpose."

My primary reason for using asSecureString is to encode the string into asterisks when typing it at the prompt. So Lee gave me two ways to convert the password to be used while invoking SetPassword. Note that unless you are using a secure LDAP channel, the password will be sent over the network in clear text.


$temporaryCredential = New-Object System.Management.Automation.PsCredential "None",$password
$newUser.psbase.Invoke("SetPassword",$temporaryCredential.GetNetworkCredential().Password)

Or, alternatively:


$temporaryCredential = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))$newUser.psbase.Invoke("SetPassword",$temporaryCredential)

If you continue to see this exception, check to make sure that the password you entered meets your domain's password complexity requirements.

PowerShell 1.0: Find the Fully Qualified Domain Name of Current Active Directory Domain

Filed under: Active Directory, PowerShell, Quick Code — Written by Chrissy on Thursday, July 26th, 2007 @ 9:52 am

So I'm making the move to PowerShell. It's painful learning such alien (to me) concepts but books like Lee Holmes' PowerShell: The Definitive Guide help a ton. I was fortunate enough to be the editor for Chapters 1-5 and got a sneak preview. It's a fantastic book and can't wait to receive the title, complete with indexes! For now, I'm searching both the 36 Word documents and the sample code for solutions using Vista's built-in search functions.

My first task, which I'll explain in later posts, includes some AD stuff. One tiny part of the equation is dynamically finding the FQDN of the current Active Directory domain. This should be easy! Using RootDSE, it's super easy to find out the DistinguishedName or even the FQDN of the domain controller being queried, so wouldn't there be a similar entry for FQDN of the whole domain? Apparently not (or if it's there, I can't find it). I've spent the morning and part of last night digging through LDAP filters, looking on Google for examples of objectcategory=crossref, dnsroot, dnshostname, etc. But after finding this useful codeplex page, I played around with GetCurrentDomain() and realized that finding the DNS hostname for an AD domain all boils down to this one line:

$strDomainDNS = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name

Or this one liner, as Lee just suggested to me

$strDomainDNS = $env:USERDNSDOMAIN

Ahh! So it was that easy. Why, then, did it take 8 hours to find that? Now I wonder how to get the FQDN of any domain. Being a newb is hard work.

If you're wondering what dnshostname is for, it's to resolve the local machine you are working with. Michael at brnets.com provides the following example embedded in a big ol Exchange script:

$rootDSE = [adsi]"LDAP://RootDSE"
$DCDNShostname = $rootDSE.dnsHostName
$DCDNShostname

Oh, as a bonus, here's some another query you may find helpful. I started with benp's script then made it a little skinnier (and more prone to errors! ;))

Search for Active Directory User Object in the Current Domain

$domain = New-Object DirectoryServices.DirectoryEntry
$search = [System.DirectoryServices.DirectorySearcher]$domain
$search.Filter = "(&(objectClass=user)(sAMAccountname=Chrissy))"
$user = $search.FindOne().GetDirectoryEntry()
$user.Name

My original code made reference to GetDirectoryEntry() but John Brennan suggested looking for alternative approaches. Thanks for the tip, John!

Fix Slow External DNS Resolution in Microsoft/Active Directory DNS Server

Filed under: Active Directory, Networking, Tech Stuff, Windows — Written by Chrissy on Wednesday, April 4th, 2007 @ 12:29 pm

Aw yaille! I just wrote up a whole explanatory blog post then lost it so this one will likely be brief... or not.

Recently, we found that one of our DNS servers was resolving external hostnames unacceptably slow -- about 5 seconds, give or take. The resolution was so slow, in fact, that all of the clients hopped on to the secondary DNS server thinking that the primary had gone down. After logging on to the server to troubleshoot, I could see that:

1. Pinging external hostnames worked well after the hostname resolved. So did traceroute.
2. Caching wasn't working at all
2. Other AD DNS servers on the network were resolving external hostnames quickly
3. The root servers were all there but I deleted and reloaded them anyway
    - Note: you can actually load root servers from a root server which is cool
4. Internal hostname resolution was extremely fast
5. A reboot didn't help (you may laugh but this has solved severe AD problems for me)

Because the other AD Servers were picking up the slack, I decided to come back to it later. I went out and had dinner with a friend then returned after a few hours. Upon logging back on to the Internets, an old network admin friend messaged me. I told him what I was seeing and he said he had the exact same issue a few months back. After a few minutes of trying to recall the solution, he asked "Have you checked your forwarders?" I'd glanced at them but went back to check again. And there it was.. an entry to a machine we'd recently taken down (long story..). I knew the moment I saw the IP that it was the problem. I removed the entry and noticed the forwarded query timeout was equal to *drumroll* 5 seconds.

Finding that solution was impossible on the Internet because of the super general terms: Slow DNS Resolution External Active Directory. Nothing really worked for me so hopefully this post will help others in the future.

Active Directory: E-mail Notification for Newly Added Users and Computers

Filed under: Active Directory, Quick Code, VBScript — Written by Chrissy on Monday, February 12th, 2007 @ 8:26 am

Recently, someone asked if there was a way to be notified when servers have been added to an Active Directory domain. I looked around the Internet and it seems there's not a direct way to do this without some large software package. So in lieu of having an instant notification, I created a script that tallies up newly added user and computer accounts and emails the admin with all the info. This script is initially setup to be run once a day, but you can modify to whatever frequency you want.

I really expected this to take at most a couple hours to write but parsing through all the data turned out to take a heckofa lot of time. From converting the desired comparison date to UTC to parsing the AD attribute memberOf, I spent a good day working on this. I learned a lot, though and found this page which details the AD schema to be very helpful.

If you need anything more than this, you may want to consider an enterprise-type administration package such as Microsoft's MOM.

ADAddedUsersNComputers.vbs

'****************************************************************************
' This script created by Chrissy LeMaire (clemaire@gmail.com)
' Website: http://netnerds.net/
'
' ADAddedUsersNComputers.vbs
'
' This script Checks AD for any additions made to Users or Computers
' in the past 24 hours. The time interval to check can be changed below.
'
' NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
'*****************************************************************************
 
'Please modify these four settings
strSMTPServer = "myexchangeserver"
strEmailFrom = "Administrator  <me@mydomain.com>"
strEmailTo = "Administrator <me@mydomain.com>"
 
strTimeInUTC = CompareDateUTCConvert("h",-24) 'This is the same syntax as dateAdd(). The example will get new users/computers added in the past 24 hours.
 
'Unless you want to change the domain to check or the format of the emailed info, nothing below really needs to be modified.
On Error Resume Next
numPersonCount = 0
numComputerCount = 0
 
Set objAdRootDSE = GetObject("LDAP://RootDSE")
Set objRS = CreateObject("adodb.recordset")
  varConfigNC = objAdRootDSE.Get("defaultNamingContext")
  strConnstring = "Provider=ADsDSOObject"
  strWQL = "SELECT ADsPath FROM 'LDAP://" & varConfigNC & "' WHERE createTimeStamp > '" & strTimeInUTC & "' and (objectCategory = 'Person' or objectCategory = 'Computer')"
 
  objRS.Open strWQL, strConnstring
      Do until objRS.eof
        Set objADUserOrComputer = GetObject(objRS.Fields.Item(0))
        strObjectCategory = ParseDN(objADUserOrComputer.objectCategory)
 
        Select Case strObjectCategory
        Case "Person"
        numPersonCount = numPersonCount + 1
            If Len(objADUserOrComputer.displayName) > 0 Then strUserMsg = strUserMsg & vbCrLf & "displayName = " & objADUserOrComputer.displayName
            'strUserMsg = strUserMsg & vbCrLf & "distinguishedName = " & objADUserOrComputer.distinguishedName
            strUserMsg = strUserMsg & vbCrLf & "sAMAccountName = " & objADUserOrComputer.sAMAccountName
            strUserMsg = strUserMsg & vbCrLf & "sAMAccountType = " & SAMAccountTypetoName(objADUserOrComputer.sAMAccountType)
            strUserMsg = strUserMsg & vbCrLf & "whenChanged = " & objADUserOrComputer.whenChanged
            strUserMsg = strUserMsg & vbCrLf & "whenCreated = " & objADUserOrComputer.whenCreated
            strUserGroups = ParseMemberOf(objADUserOrComputer.memberOf,objADUserOrComputer.PrimaryGroupID)
            strUserMsg = strUserMsg & vbCrLf & "Member Of: " & strUserGroups
            If Len(objADUserOrComputer.userPrincipalName) > 0 Then strUserMsg = strUserMsg & vbCrLf & "userPrincipalName = " & objADUserOrComputer.userPrincipalName
            strUserMsg = strUserMsg & vbCrLf
         Case "Computer"
         numComputerCount = numComputerCount + 1
            strCompMsg = strCompMsg & vbCrLf & "dNSHostName = " & objADUserOrComputer.dNSHostName
            strCompMsg = strCompMsg & vbCrLf & "isCriticalSystemObject = " & objADUserOrComputer.isCriticalSystemObject
            strCompMsg = strCompMsg & vbCrLf & "operatingSystem = " & objADUserOrComputer.operatingSystem
            strCompMsg = strCompMsg & vbCrLf & "operatingSystemServicePack = " & objADUserOrComputer.operatingSystemServicePack
            strCompMsg = strCompMsg & vbCrLf & "operatingSystemVersion = " & objADUserOrComputer.operatingSystemVersion
            If InStr(objADUserOrComputer.rIDSetReferences,"Domain Controller") > 0 Then strCompMsg = strCompMsg & vbCrLf & "Domain Controller = Yes"
            If Len(objADUserOrComputer.description) > 0 Then strCompMsg = strCompMsg & vbCrLf & "description = " & objADUserOrComputer.description
             If Len(objADUserOrComputer.machineRole) > 0 Then strCompMsg = strCompMsg & vbCrLf & "machineRole = " & objADUserOrComputer.machineRole
            If Len(objADUserOrComputer.physicalLocationObject) > 0 Then strCompMsg = strCompMsg & vbCrLf & "physicalLocationObject = " & ParseDN(objADUserOrComputer.physicalLocationObject)
            strCompMsg = strCompMsg & vbCrLf
         End Select
        objRS.movenext
        Set objADUserOrComputer = Nothing
      Loop
    objRS.close
Set objRS = Nothing
Set objAdRootDSE = Nothing
 
If Len(strUserMsg) > 0 Then strEmailMessage = strEmailMessage & "--------- USERS ---------" & vbCrLf & strUserMsg & vbCrLf
If Len(strCompMsg) > 0 Then strEmailMessage = strEmailMessage & "--------- COMPUTERS ---------" & vbCrLf & strCompMsg
If Len(strUserMsg) = 0 And Len(strCompMsg) = 0 Then strEmailMessage = "No users or computers have been added in the last 24 hours."
 
Set objCDO = CreateObject("CDO.Message")
    objCDO.Subject = "Users Added: " & numPersonCount & ". Computers Added: " & numComputerCount & "."
    objCDO.From = strEmailFrom
    objCDO.To = strEmailTo
    objCDO.TextBody = strEmailMessage
    objCDO.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2 'cdoSendUsingPort (1 = local, 3 = Exchange)
    objCDO.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = strSMTPServer
    objCDO.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
    objCDO.Configuration.Fields.Update
    objCDO.Send
    set objCDO = Nothing
 
Function CompareDateUTCConvert(dateAddInterval,compareNumber)
'Wow, this is a lil complex. So createTimestamp is in UTC format.
'So first we grab your machine's time bias and then apply it.
'Next, we adjust the date to the one you specified above (now()-24hours by default)
'Finally, we parse the final date to UTC format ie. 20070207032200.0Z
 
Set objSWbemServices = GetObject("winmgmts:" & "{impersonationLevel=impersonate}!\.\root\cimv2")
  Set colTimeZone = objSWbemServices.ExecQuery("SELECT * FROM Win32_TimeZone")
    For Each objTimeZone in colTimeZone
     strBias = objTimeZone.Bias
    Next
  Set colTimeZone = Nothing
Set objSWbemServices = Nothing
  strCompareDate = dateAdd(dateAddInterval,compareNumber,now())
  strUTCCompare = DateAdd("n",strBias*(-1),strCompareDate)
  CurrentUTC = Year(strUTCCompare) & Right("0" & Month(strUTCCompare),2) & Right("0" & Day(strUTCCompare),2)
  CurrentUTC = CurrentUTC & Right("0" & Hour(strUTCCompare),2) & Right("0" & Minute(strUTCCompare),2) & Right("0" & Second(strUTCCompare),2) & ".0Z"
  CompareDateUTCConvert = CurrentUTC
End Function
 
Function ParseDN(strDN)
'Take a DN and extract what we want then make it pretty.
  arrDN = split(strDN,",") 'CN=Example-Thing,CN=Whatever,CN=Etc
  strDN = right(arrDN(0),len(arrDN(0))-3) 'CN=Example-Thing -> Example-Thing
  strDN = replace(strDN,"-"," ") 'Example Thing
  ParseDN = strDN
End Function
 
Function ParseMemberOf(memberof,primarygroupid)
'This shows what groups a person belongs to.
'The output of memberof changes depending on
'how many groups the user is a member of, etc.
  Select Case TypeName(memberof)
    Case "String" ParseMemberOf = ParseDN(memberof)
    Case "Empty" ParseMemberOf = PrimaryGroupIDtoName(primarygroupid,varConfigNC)
    Case "Variant()"
      For each groupDN in memberof
         strUserGroups = strUserGroups & vbCrLf & ParseDN(groupDN)
      Next
      ParseMemberOf = strUserGroups
    Case Else ParseMemberOf = "Unknown"
  End Select
End Function
 
Function SAMAccountTypetoName(theType)
'Just makin it more useful...
  Select Case theType
    Case 268435456 SAMAccountTypetoName = "Group Object"
    Case 268435457 SAMAccountTypetoName = "Non-Security Group Object"
    Case 536870912 SAMAccountTypetoName = "Alias Object"
    Case 536870913 SAMAccountTypetoName = "Non-Security Alias Object"
    Case 805306368 SAMAccountTypetoName = "Normal User Account"
    Case 805306369 SAMAccountTypetoName = "Machine Account"
    Case 805306370 SAMAccountTypetoName = "Trust Account"
    Case 1073741824 SAMAccountTypetoName = "App Basic Group"
    Case 1073741825 SAMAccountTypetoName = "App Query Group"
    Case 2147483647 SAMAccountTypetoName = "Account Type Max"
    Case Else SAMAccountTypetoName = "Unknown"
  End Select
End Function
 
Function PrimaryGroupIDtoName(PGID,varConfigNC)
'Ugh.. the alternative to this function can be found here:
'http://support.microsoft.com/kb/297951
'both are kinda nasty.
  Set objRSPGID = CreateObject("adodb.recordset")
    Connstring = "Provider=ADsDSOObject"
    strSQL = "<ldap://" & varConfigNC & ">;(objectCategory=group);distinguishedName,primaryGroupToken,name;subtree"
     objRSPGID.Open strSQL, Connstring
      If not objRSPGID.eof and not objRSPGID.bof Then
        Do until objRSPGID.eof Or Len(strGroupName) > 0
        If PGID = objRSPGID("primaryGroupToken") Then strGroupName = objRSPGID("name")
         objRSPGID.movenext
        Loop
      End If
     objRSPGID.close
  Set objRSPGID = Nothing
  If Len(strGroupName) = 0 Then strGroupName = "Unknown"
  PrimaryGroupIDtoName =   strGroupName
End Function

To schedule this, save the above code as ADAddedUsersNComputers.vbs in C:\scripts (for ex.) and use Scheduled Tasks to run the following command: %windir%\system32\wscript.exe C:\scripts\ADAddedUsersNComputers.vbs. I suggest running it daily at the end of each workday.

VBScript: Enumerate All SQL Servers on a Domain

Filed under: Active Directory, Quick Code, VBScript — Written by Chrissy on Tuesday, January 9th, 2007 @ 11:14 pm

So if you have SQL Server installed locally, you're lucky enough to have access to SQLDeeMO. You can then easily enumerate SQL Servers with the following code. Note: If you do decide to use VBScript and SQLDeeMO, you will need to remove the "ee" in the script below. I didn't want to put in the actual object name so that people searching Google for sites that do not contain the phrase SQLDeeMO will still find my site.

SQLDeeMO VBScript Action

    Set objSQLDMOApp = CreateObject("SQLDeeMO.Application")
 
    Set objSQLList = objSQLDeeMOApp.ListAvailableSQLServers()
    For i = 1 To objSQLList.Count
       MsgBox objSQLList.Item(i)
    Next
 
    Set objSQLList = Nothing
    Set objSQLDeeMOApp = Nothing

But if you don't have SQL Server installed locally.. here's a hack that grabs the name of all Windows Servers in AD and then checks their registry for instances of SQL Server.

Enumerate All SQL Servers and Instance Names on a Domain

'****************************************************************************
' This script created by Chrissy LeMaire (clemaire@gmail.com)
' Website: http://netnerds.net/
'
' This script finds all SQL Servers and their instances that are members of an AD domain
' and running any Windows Server version
'
' Run this script with admin privs on any computer within a domain
'
' This script has only been tested on Windows Server 2003
'
' "What it does"
' 1. Gathers all machines in a domain that are running a Windows Server OS (NT, 2000, 2003)
' 2. Pings them to see if they are available
' 3. If they do respond to pings, it checks their registry to see if they have the proper SQL keys
' 4. If the key does exist, it then enumerates the instances (including default)
'
' NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
'*****************************************************************************
Set objAdRootDSE = GetObject("LDAP://RootDSE")
Set objWMIService = GetObject("winmgmts:\.\root\cimv2")
Set objRS = CreateObject("adodb.recordset")
Const HKEY_LOCAL_MACHINE = &H80000002
 
  varConfigNC = objAdRootDSE.Get("defaultNamingContext")
  strConnstring = "Provider=ADsDSOObject"
  strWQL = "SELECT * FROM 'LDAP://" & varConfigNC & "' WHERE objectCategory= 'Computer' and OperatingSystem = 'Windows*Server*'"
  objRS.Open strWQL, strConnstring
      Do until objRS.eof
         Set objServer = GetObject(objRS.Fields.Item(0))
          strServerName = objServer.CN
 
          Set colItems = objWMIService.ExecQuery("Select * from Win32_PingStatus Where Address = '" & objServer.DNSHostName & "'")
          For Each objItem in colItems
              If objItem.StatusCode = 0 Then 'The Computer is Pingable
                    Set objRegistry = GetObject("winmgmts:{impersonationLevel=impersonate}!" & strServerName & "\root\default:StdRegProv")
                      strKeyPath = "SOFTWARE\Microsoft\Microsoft SQL Server"
                      strValueName = "InstalledInstances"
                      objRegistry.GetMultiStringValue HKEY_LOCAL_MACHINE,strKeyPath, strValueName,arrValues
                        If IsNull(arrValues) = 0 Then 'It's a SQL Server! Enumerate it's instances
                          strMsg = strServerName & " is running the following SQL Instance(s): "
                          For Each strValue In arrValues
                              If strValue = "MSSQLSERVER" Then strValue = "Default Instance"
                                strMsg = strMsg & vbCrLf & strValue
                          Next
                          MsgBox strMsg
                        End If
                     Set objRegistry = Nothing
              End If
          Set objServer = Nothing
      Next
    objRS.movenext
    Loop
  objRS.close
 
Set objWMIService = Nothing
Set objRS = Nothing
Set objAdRootDSE = Nothing

If you are looking for the version of each SQL Server.. you can either query it with SELECT @@VERSION or it may be easily found in the registry. It's 10:47pm and I'm still at work so I'm heading out ;)

VBScript: Use an LDAP Query to Find All Windows Servers on a Domain

Filed under: Active Directory, Networking, Quick Code, VBScript — Written by Chrissy on Tuesday, January 9th, 2007 @ 9:02 pm

Damn, the ADsDSOObject rocks! This script, which weighs in at less than 20 lines, finds all machines running any form of Windows Server on a given domain. Note that this script isn't useful in finding domain controllers, but rather any machine running Windows Server.

Awesome

'****************************************************************************
' This script created by Chrissy LeMaire (clemaire@gmail.com)
' Website: http://netnerds.net/
'
' This script finds all machines running Windows Server (NT, 2000, 2003) in AD
'
'Msgbox output provides server name and OS version.
'
' NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
'*****************************************************************************
 
Set objAdRootDSE = GetObject("LDAP://RootDSE")
Set objRS = CreateObject("adodb.recordset")
 
  varConfigNC = objAdRootDSE.Get("defaultNamingContext")
  strConnstring = "Provider=ADsDSOObject"
  strWQL = "SELECT * FROM 'LDAP://" & varConfigNC & "' WHERE objectCategory= 'Computer' and OperatingSystem = 'Windows*Server*'"
  objRS.Open strWQL, strConnstring
    Do until objRS.eof
       Set objServer = GetObject(objRS.Fields.Item(0))
      strServerName = objServer.CN
      strOperatingSystem = objServer.OperatingSystem
      MsgBox strServerName & " is running " & strOperatingSystem
       objRS.movenext
       Set objServer = Nothing
    Loop
  objRS.close
 
Set objRS = Nothing
Set objAdRootDSE = Nothing

Also, I found this nice reference of Command One Liners while searching the web. Totally handy!

VBScript: Find All Exchange Servers in Active Directory

Filed under: Active Directory, Exchange, Quick Code, VBScript — Written by Chrissy on Tuesday, January 9th, 2007 @ 7:44 pm

My friend Sharfa and I were exchanging some of our favorite code snippets and he showed me one for enumerating Exchange Servers in Active Directory. I dug the code but wanted to try to see if I could use my Recordset/ADsDSOObject skrills to shorten the code. The outcome isn't any shorter but it does get the version, so that's cool. Thanks, Sharfa, for pointing me towards the WMI Exchange_Server thing, too. :-D
Hit It

'****************************************************************************
' This script created by Chrissy LeMaire (clemaire@gmail.com)
' Website: http://netnerds.net/
'
' This script finds all Exchange Servers in AD. Includes Exchange Version.
'
' Run this script with admin privs on any computer within a domain.
'
' This script has only been tested on Windows Server 2003
'
' NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
'*****************************************************************************
 
Set objAdRootDSE = GetObject("LDAP://RootDSE")
Set objRS = CreateObject("adodb.recordset")
 
  varConfigNC = objAdRootDSE.Get("configurationNamingContext")
 
  strConnstring = "Provider=ADsDSOObject"
  strSQL = "SELECT * FROM 'LDAP://" & varConfigNC & "' WHERE objectCategory='msExchExchangeServer'"
  objRS.Open strSQL, strConnstring
    Do until objRS.eof
      Set objServer = GetObject(objRS.Fields.Item(0))
        Call getExchangeInfo(objServer.CN)
      Set objServer = Nothing
      objRS.movenext
    Loop
  objRS.close
 
Set objRS = Nothing
Set objAdRootDSE = Nothing
 
Sub getExchangeInfo(strServerName)
  Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!" & strServerName & "\ROOT\MicrosoftExchangeV2")
  Set colItems = objWMIService.ExecQuery("Select * from Exchange_Server")
 
    For Each objItem in colItems
      MsgBox UCase(objItem.Name) & " (" & objItem.FQDN & ") is running Exchange " & objItem.ExchangeVersion
    Next
 
  Set colItems = Nothing
  Set objWMIService = Nothing
End Sub

VBScript: Kerberos, Delegation, IIS and User Authentication

Filed under: Active Directory, IIS, Quick Code, VBScript — Written by Chrissy on Wednesday, December 13th, 2006 @ 9:30 am

Recently, I wanted to write a web-based front end to AD User Management for our help desk. The way that I set it up apparently broke some Kerberos delegation rules and even though Microsoft wrote a step-by-step guide on how to get IIS and Kerberos delegation going, the solution didn't work for me. If I turned off Anonymous access and authenticated as myself against a remote webserver (local webserver totally worked), I would get the error 0x80040E37 - Table Does Not Exist. The table does exist, of course, I just don't have the rights to see it. Well, I do but not in the "double hop" manner that I'm attempting it. Kerberos sees that IIS != me.. IIS is only pretending to be me and it doesn't approve.

After a few days of tinkering, this is the solution I decided on. I set the ASP page in IIS to Anonymous Access but ran that access as a user with the privleges to make changes to user accounts. This can be dangerous in more than one way -- if a lesser-privleged user were to somehow have the ability to edit that page, he or she could use it to run any script under the power of that user. So I ensured that unauthorized users were not able to access that file. But now how can I detect who's running that page? Request.servervariables("REMOTE_USER") would be useless since the page is running as that privleged user. I considered what client variables I did have access to and realized that request.servervariables("REMOTE_HOST") would be the handiest. I figured that, using WMI and the IP address, it would be possible to figure out what user is actively logged into the client machine. Sure enough...

Function getLoggedInUser(ipaddr)
  Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!"& ipaddr & "\root\cimv2")
  Set colSessions = objWMIService.ExecQuery("Select * from Win32_LogonSession Where LogonType = 2 OR LogonType = 10" )
 
  For Each objSession in colSessions
   Set colList = objWMIService.ExecQuery("Associators of {Win32_LogonSession.LogonId=" & objSession.LogonId & "} Where AssocClass=Win32_LoggedOnUser Role=Dependent")
    For Each objItem in colList
    getLoggedInUser = lcase(objItem.Name)
    Next
  Next
End Function

This script, which was derived from a tek-tips.com post, worked flawlessly! FWIT, LoginType 2 is console and 10 is remote desktop.

AD: Active Directory Users and Computers Slow to Initialize

Filed under: Active Directory, General — Written by Chrissy on Friday, December 1st, 2006 @ 12:27 am

Recently, two of the network administrators at my firm ran into an issue with with the Active Directory Users and Computers MMC snap-in. ADUC often loaded slowly. Sometimes rebooting would fix the issue, sometimes it wouldn't. I never had an issue but I didn't use ADUC as often on my workstation (I use it primarily on the DCs themselves) and I was also in another city. The two network admins were in San Diego with the operations master, I was in San Francisco with an outer city DC.

We searched the web and found an unhelpful Microsoft KB article promisingly titled "Active Directory MMC Tools Are Slow to Initialize. All of our DNS records appeared just as they should.

Each time one of the network admins, Jeff, would encounter the problem, he would call me to see if my ADUC snap-in would load quickly -- I never had any issues. I wondered if it was an issue with MMC.exe or with LDAP itself. I asked him to run the following script derived from the Active Directory Cookbook when he was having issues and report back to me the time that would pop up:

startTime = timer()
DisplayObjects "LDAP://OU=Users,DC=San Diego,DC=OurDomain,DC=com", ""
Function DisplayObjects( strADsPath, strSpace)
   set objObject = GetObject(strADsPath)
   for each objChildObject in objObject
      DisplayObjects objChildObject.ADsPath, strSpace & " "
   next
End Function
FinishTime = timer()
 
totalTime = finishTime-startTime
msgbox "This script took " &  totalTime & " seconds to execute."

This script, which iterates through each of the users and their child objects in our San Diego Users OU, usually takes about 0.3 seconds to run. When Jeff ran into the issue, it would take up to 40 seconds. After we confirmed that his issue with his network or LDAP connection, not the actual MMC, we began to troubleshoot at a lower level -- this likely isn't an anti-virus+mmc issue that some people have reported.

I then asked Jeff to rerun the script I sent but this time, we would specifically query the San Diego DC. He changed the "LDAP://OU=Users" string to "LDAP://SanDiegoDC/OU=Users". Bingo! The script's time reported finishing in 0.3 seconds as opposed to 20 seconds. I then had him open up ADUC, right click on the domain icon and select "Connect to specific domain controller". The dialog box confirmed that he was connected to an outer office DC and the default was set to "Any Writable Domain Controller." We set that manually to the San Diego DC and ADCU instantly became more responsive.

Finding a way to set the San Diego DC to be the default for the Active Directory Users and Computers MMC snap-in was a challenge. I checked the registry but didn't find much. Finally, I used The Google and found that he could create a shortcut which opens up the proper DC.

dsa.msc /server=sandiegodc.ourdomain.com

I'll admit, that's a bit of a band-aid but it will permanently solve Jeff's ADMC problem. I still need to dig to figure out why his machine thinks that the outer office DC is the closest DC. Time to re-check our AD Sites and Services setup...