Access to information stored in Active Directory is available using standard Active Directory management utilities. They are convenient and fairly easy to use; however, there are situations in which you might want to develop alternative, custom solutions. For example, you might want to quickly access user or group account information from any workstation in your environment. Of course, you can use Terminal Services to accomplish this, but this has certain limitations that you need to consider:
- Without having Terminal Services running in Application mode, you are limited to only two concurrent connections per server.
- By default, permission for accessing Terminal Services in Administration mode is limited to local Administrators of the target server only. This means that you either need to ensure that everyone accessing the server is its local Administrator or allow non-Administrative groups to connect. Either workaround negatively impacts the level of security.
These limitations can be eliminated by providing access to Active Directory through Web-based ASP scripts. Active Server Pages (ASP) is the Internet Information Server technology that allows dynamic generation of HTML pages. Scripts, typically written in VBScript or JScript, are executed when accessed by a client running a browser. The results of this execution are sent back to the Web client in HTML format and displayed in the browser window. Scripts running on a Web server are in some aspects unlike those invoked locally, the differences of which need to be taken into account:
- Locally executed scripts are run typically by one user at a time, while those run on a server are commonly executed by multiple users at the same time. This increases performance load and contention when accessing Web server resources.
- The results of the ASP scripts are displayed in the browser window, while those run locally typically use other means of output.
- Dealing with authentication mechanisms is more complex when executing Web-based scripts.
Obviously, there are numerous other issues that need to be considered in both cases, but we will cover these three, since they tend to be most relevant in the situation presented in this article. In particular, we will look into execution of scripts that are using ADSI to access Active Directory information.
First, I'll present a script that runs outside of ASP (locally) and then I'll describe a way to convert to a Web-based form.
The script below, ListADGroups.vbs, uses Active Data Objects (and an underlying OLE DB Directory Services provider called ADsDSOObject) to extract a list of groups from Active Directory. This involves creating the following ADO objects:
- A Connection object stored in oCon variable,
- A Command object stored in oCmd variable,
- A RecordSet object, stored in oRecordSet variable.
For each of these objects, we set appropriate properties. More specifically, for the Connection object:
- Provider property determines OLE DB provider used for connection to Active Directory (ADSDSOObject)
- sUser variable is the name of the domain user that will be used to connect to Active Directory (replace this value with an actual user name in your domain)
- sPassword variable is a password for the sUser account (set it appropriately as well)
For the Command Object:
- ActiveConnection property is set to the previously created oCon object
- CommandText property consists of semicolon-separated search parameters. The scope is determined by the first parameter, which contains, in this case, the LDAP path of the target domain. The second parameter contains search criteria. objectCategory determines the type of object to search for (groups in our case). The objects are filtered according to the value of the sGroup variable. For example, in order to return all the groups with names that start with the "US" prefix, you would set sGroup to "US*". The value of "*" used in the script returns all the groups within the scope determined by the first parameter (domain in this case). Object properties to be returned are listed in the third parameter (and include group name, LDAP path, description, and group members). Finally, the last parameter specifies search depth (in the hierarchy of Active Directory containers) -- all directory objects in our case.
The outcome of running the Execute method of the ADO Command object is stored in the RecordSet. In order to retrieve the results, the script traverses it record by record and displays contents of each one.
After all the results are listed, Recordset and Connection objects are closed and set to Nothing, which releases them from memory.
'////////////////////////////////////////////////////////////////////////// '/// Name: ListADGroups.vbs '/// Version: 1.0 '/// Date: 09/17/02 '/// Purpose: listing information about Active Directory groups '/// OS: Windows 2000, XP '/// Reqs: Account with permissions to read Active Directory groups properties '/// Syntax: cscript /nologo ListADGroups.vbs '////////////////////////////////////////////////////////////////////////// Option Explicit On Error Resume Next '//////////////////////////////////////////////////// '/// Variable Declarations Dim oRootDSE, oCon, oCmd, oRecordSet Dim sDomainADsPath, sUser, sPassword, sGroup, sProperties Dim aDescription, aMember, iCount '//////////////////////////////////////////////////// '/// Extract domain name of the logged on user account Set oRootDSE = GetObject("LDAP://RootDSE") sDomainADsPath = "LDAP://" & oRootDSE.Get("defaultNamingContext") Set oRootDSE = Nothing '//////////////////////////////////////////////////// '/// Create, configure, and open ADO Connection object Set oCon = CreateObject("ADODB.Connection") oCon.Provider = "ADsDSOObject" sUser = "UserName" sPassword = "Password" oCon.Open "ADProvider", sUser, sPassword '//////////////////////////////////////////////////// '/// Create and configure ADO Command object Set oCmd = CreateObject("ADODB.Command") Set oCmd.ActiveConnection = oCon sProperties = "name,ADsPath,description,member" sGroup = "*" oCmd.CommandText = "<" & sDomainADsPath & ">;(&(objectCategory=group)(name=" & sGroup & "));" & sProperties & ";subtree" oCmd.Properties("Page Size") = 100 '//////////////////////////////////////////////////// '/// Populate ADO RecordSet object with AD Group info Set oRecordSet = oCmd.Execute '//////////////////////////////////////////////////// '/// Display results by listing all records in Recordset WScript.Echo "Global Groups for the domain " & Replace(Mid(sDomainADsPath,11), ",DC=", ".") While Not oRecordSet.EOF WScript.Echo "Name: " & vbTab & oRecordSet.Fields("name") WScript.Echo "ADsPath: " & vbTab & oRecordSet.Fields("ADsPath") aDescription = oRecordSet.Fields("description") If Not IsNull(aDescription) Then WScript.Echo "Description: " & vbTab & aDescription(0) End If aMember = oRecordSet.Fields("member") WScript.Echo "Members: " If Not IsNull(aMember) Then For icount = 0 to UBound(aMember) WScript.Echo vbTab & vbTab & aMember(iCount) Next End If oRecordSet.MoveNext Wend '//////////////////////////////////////////////////// '/// Close Recordset and Connection objects oRecordSet.Close oCon.Close '//////////////////////////////////////////////////// '/// Clean up Set oRecordSet = Nothing Set oCon = Nothing
In order to adapt this script to a Web Server environment, the following changes (at the very least) need to be made:
- The script should be enclosed within the <% and %> marks, which designate parts of the HTML page that are processed before being sent back to the client
- Createobject entries should be changed to Server.CreateObject (Server is an ASP object), which eliminates contention issues when multiple users try to run the same script
- Response.Write statements should be used to generate HTML code within the scripted portion of the page
The ASP page below contains all these changes. The formatting is very crude for simiplicity reasons. The script generates a table containing four columns: group name, ADsPath, description, and members list (members list is formatted as a listbox). The page generation might take a while in larger domains, due to the fact that, by default, the value of the sGroup variable (search filter) is set to a wild card ("*"), which returns all the domain groups. If you want to narrow down your search and speed up the return of the results, replace the "*" with a character string indicating a match (e.g. to return all the groups starting with the word domain, use "Domain*").
<%@ Language="VBScript" %> <% Option Explicit %> <HTML> <HEAD> <TITLE>Listing of Domain Groups</TITLE> </HEAD> <% Dim oRootDSE, oCon, oCmd, oRecordSet Dim sDomainADsPath, sUser, sPassword, sGroup, sProperties Dim aDescription, aMember, iCount Set oRootDSE = GetObject("LDAP://RootDSE") sDomainADsPath = "LDAP://" & oRootDSE.Get("defaultNamingContext") Set oRootDSE = Nothing Set oCon = Server.CreateObject("ADODB.Connection") sUser = "UserName" sPassword = "Password" oCon.Provider = "ADsDSOObject" oCon.Open "ADProvider", sUser, sPassword Set oCmd = Server.CreateObject("ADODB.Command") Set oCmd.ActiveConnection = oCon sProperties = "name,ADsPath,description,member" sGroup = "*" oCmd.CommandText = "<" & sDomainADsPath & ">;(&(objectCategory=group)(name=" & sGroup & "));" & sProperties & ";subtree" oCmd.Properties("Page Size") = 100 Set oRecordSet = oCmd.Execute Response.Write("<strong> Global Groups for the domain: " & Replace(Mid(sDomainADsPath,11), ",DC=", ".") & "</strong>") Response.Write("<table border='1'>") Response.Write("<tr><th>Name</th><th>ADsPath</th><th>Description</th><th>Members</th></tr>") Response.Write("<font size=-2>") While Not oRecordSet.EOF Response.Write("<tr><td>" & oRecordSet.Fields("name") & "</td>") Response.Write("<td>" & oRecordSet.Fields("ADsPath") & "</td>") aDescription = oRecordSet.Fields("description") Response.Write("<td> ") If Not IsNull(aDescription) Then Response.Write aDescription(0) Response.Write("</td>") aMember = oRecordSet.Fields("member") Response.Write("<td><select size = '5'> ") If Not IsNull(aMember) Then For icount = 0 to UBound(aMember) Response.Write("<option>" & aMember(iCount)) Next End If Response.Write("</td></tr>") oRecordSet.MoveNext Wend Response.Write("</font>") Response.Write("</table>") oRecordSet.Close oCon.Close Set oRecordSet = Nothing Set oCon = Nothing %> </BODY> </HTML>
As you can see, the content of the script has not changed much -- the main differences result from the fact that the output needs to be formatted according to HTML specifications. However, there is another significant issue that, although present in the original script, becomes more relevant in a Web environment. As you must have noticed, username and password are stored within a script. Even though such pages would be most likely secured through additional level of restrictions, such behavior should be avoided. In the next article of this series, I'll present various ways of eliminating this vulnerability.