Wednesday, April 20, 2005

Reverse msExchMailboxSecurityDescriptor Permissions Audit script

There are a few places where you can assign mailbox rights with Exchange 2x one of these is from Active Directory User and Computer via the Mailbox rights tab which manipulates the msExchMailboxSecurityDescriptor active directory attribute. You can display who has access to a mailbox using a script which reads this attribute and there is a good sample of doing this here .

But if you want to go the other way by displaying what mailboxes a particular User account has access to this can be a little more challenging. Essentially you need to enumerate the msExchMailboxSecurityDescriptor of every account in your domain and then relate this data. Fortunately this is where ADO Data shaping comes in real handy I’ve used data shaping before here . Data shaping allows you to build hierarchal data structures which in this case allows you to build a structure where the ACE objects (Trustees) on each mailbox relates to the User Account in Active Directory.

So to put this together in a script you have something that selects all the mailboxes in a domain using a LDAP query and then enumerates the ACE's on each mailbox one at time. It then populates two disconnected record-sets and then shapes these together by relating the trustee names to the user account name (using domain\username format)

This script only checks for ACE’s that have been explicitly set on the mailbox it. It will ignore the self permission and also any ACE's that are inherited from the mail store. This narrows the output of the script so it only shows which mailbox a user has been explicitly granted access to from AD Users and Computers. It’s very possible that you may have no mailboxes that meet these criteria so in this case the script would not output anything. If you have a large number of mailboxes this script can take a while to run because its essentially doing a query on every mailbox in your domain.

What’s it good for ? Mostly auditing it gives you another view of the data eg you can see that fred bloggs has access to the following mailbox instead of looking at it form the other perspective of checking each mailboxes ACL. This can help you see permission changes that might have flown under the Radar. What it doesn’t do is look at MAPI permissions so where mailboxes or folders have been delegated using Outlook for this you would need to logon to every mailbox using something like CDO to grab all these rights. While this is possible to do it can be complicated in a large environment I might look at doing this a bit later so you can look for example from a user perspective what calendars a user has been delegated rights to.

I put a downloadable copy of the script here the code looks like

Const RIGHT_DS_DELETE = &H10000
Const RIGHT_DS_READ = &H20000
Const RIGHT_DS_CHANGE = &H40000

Set objSystemInfo = CreateObject("ADSystemInfo")
strdname = objSystemInfo.DomainShortName
set conn1 = createobject("ADODB.Connection")
strConnString = "Data Provider=NONE; Provider=MSDataShape"
conn1.Open strConnString
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
Query = "<LDAP://" & strNameingContext & ">;(&(&(& (mailnickname=*) (| (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)(msExchHomeServerName=*)))
Com.ActiveConnection = Conn
Com.CommandText = Query
Com.Properties("Page Size") = 1000
set objParentRS = createobject("adodb.recordset")
set objChildRS = createobject("adodb.recordset")
" NEW adVarChar(255) AS UOADDisplayName, " & _
" NEW adVarChar(255) AS UOADTrusteeName, " & _
" ((SHAPE APPEND " & _
" NEW adVarChar(255) AS MRmbox, " & _
" NEW adVarChar(255) AS MRTrusteeName, " & _
" NEW adVarChar(255) AS MRRights, " & _
" NEW adVarChar(255) AS MRAceflags) " & _
" RELATE UOADTrusteeName TO MRTrusteeName) AS rsUOMR"
objParentRS.LockType = 3
objParentRS.Open strSQL, conn1

Set Rs = Com.Execute
While Not Rs.EOF
dn = "LDAP://" & replace(rs.Fields("distinguishedName").Value,"/","\/")
set objuser = getobject(dn)
Set oSecurityDescriptor = objuser.Get("msExchMailboxSecurityDescriptor")
Set dacl = oSecurityDescriptor.DiscretionaryAcl
Set ace = CreateObject("AccessControlEntry")
objParentRS("UOADDisplayName") = rs.fields("displayname")
objParentRS("UOADTrusteeName") = strdname & "\" & rs.fields("samaccountname")
Set objChildRS = objParentRS("rsUOMR").Value
For Each ace In dacl
if ace.AceFlags <> 18 then
if ace.Trustee <> "NT AUTHORITY\SELF" then
objChildRS("MRmbox") = rs.fields("displayname")
objChildRS("MRTrusteeName") = ace.Trustee
objChildRS("MRRights") = ace.AccessMask
objChildRS("MRAceflags") = ace.AceFlags
end if
end if
wscript.echo "Number of Mailboxes Checked " & objParentRS.recordcount
Do While Not objParentRS.EOF
Set objChildRS = objParentRS("rsUOMR").Value
if objChildRS.recordcount <> 0 then wscript.echo objParentRS("UOADDisplayName")
Do While Not objChildRS.EOF
wscript.echo " " & objChildRS.fields("MRmbox")
If (objChildRS.fields("MRRights") And RIGHT_DS_SEND_AS) Then
wscript.echo " -send mail as"
End If
If (objChildRS.fields("MRRights") And RIGHT_DS_CHANGE) Then
wscript.echo " -modify user attributes"
End If
If (objChildRS.fields("MRRights") And RIGHT_DS_DELETE) Then
wscript.echo " -delete mailbox store"
End If
If (objChildRS.fields("MRRights") And RIGHT_DS_READ) Then
wscript.echo " -read permissions"
End If
If (objChildRS.fields("MRRights") And RIGHT_DS_TAKE_OWNERSHIP) Then
wscript.echo " -take ownership of this object"
End If
If (objChildRS.fields("MRRights") And RIGHT_DS_MAILBOX_OWNER) Then
wscript.echo " -is mailbox owner of this object"
End If
If (objChildRS.fields("MRRights") And RIGHT_DS_PRIMARY_OWNER) Then
wscript.echo " -is mailbox Primary owner of this object"
End If

Thursday, April 14, 2005

ASP.NET and Exchange tips and samples

I’ve been writing some ASP.NET pages for my Exchange boxes at the moment and although there is a lot of good information out there about ASP.Net finding good information about using ASP.Net with Exchange can be a little challenging so I though I share some info and links that I found useful. For this post I’m looking at using NTLM authentication on a remote web server to access Exchange 200x box on another server (if your using Form Based Authentication have a look at this )


This was one of the hardest things for me to get my head around from classic asp. With “ASP.NET it provides an out-of-process execution model, which protects the server process from user code” this provides a good primer on the ASP.NET process model

The import part of this is what account the ASP.NET process runs under by default in IIS5 it runs under a local SAM account “ASPNET”. On an IIS6 box (win 2003) the ASP.NET process runs under the default Network Service (system account) account. This becomes important when you start to try and delegate authentication to access another server.


Impersonation is important because it allows you to access a mailbox in your ASP.Net page using the credentials of the user you have authenticated on your webserver. The important thing to remember about impersonation is that by itself it only allows you to access resources on the local server using the credentials you’re impersonating. There is a good KB article on impersonation that gives you the different methods you can use to perform impersonation from using the web.config file to doing it in-code. I like the in-code method to me its a more flexible and secure way to do it.


“Impersonation enables ASP.NET to execute code and access resources in the context of an authenticated and authorized user, but only on the server where ASP.NET is running. To access resources located on another computer on behalf of an impersonated user requires authentication delegation (or delegation for short).” Ref this

This is pretty important thing if you want your remote web server to be able to access your mailbox. Another import thing from the “Integrated Windows Authentication” section is “When used in conjunction with Kerberos v5 authentication, IIS can delegate security credentials among computers running Windows 2000 or later that are trusted and configured for delegation.”. Which means to get delegation to work you need to be using Kerberos and you need to go off and read another couple of articles which details how to set this up this msdn article and Q810572. The import part in these articles is if you’re running IIS5 in the default config “the account used to run the server process (the process that performs impersonation) is allowed to delegate client accounts. You must configure the user account under which the server process runs, or if the process runs under the local SYSTEM account.”. On a IIS5 box the default user is the aspnet user which means you will need to change the account being used to one that can be trusted for delegation (eg the system account).

(Note: this was one thing that I found really hard to get good accurate information on what I’ve included in this post is what I found worked for me. Running the process under the system account does open up certain security issues but hey so does enabling delegation on your web server if someone wishes’s to correct me please send me an email. If you’re using 2003 this conversation is kind of redundant plus you have the ability to do constrained delegation)

SSL and Self signed certificates

Running WebDAV over http remotely is not a great idea because your going to be shipping your data as clear text over the network So its best to use https to do all your conversations with the Exchange server. If you’re down the less financially able spectrum you may not be able to afford to be using registered SSL certificates. In this case your application may have to deal with certificate warning messages. You can do this “by implementing your own CertificatePolicy class (which implements the ICertificatePolicy interface). In this class you will have to write your own CheckValidationResult function that has to return true or false” have a read of which gives a good description and some code to work around this.

Some Examples

I’ve come up with a couple of samples that demonstrate all the stuff I’ve talked about. Both samples do the same thing they use the System.Directroyservices namespace to build a URL to access the user’s logon information (based on the user that is authenticated on the IIS server). Once the URL is built it then connects to the mailbox and queries all the contacts in a user’s mailbox. The first example uses a XSL file to do a transform of the XML returned from the WebDAV query. They both use SSL and implement a method to override any certificate popup warnings. The second example loads the result of the WebDAV query into an ADO.NET dataset and then displays the result in a datagrid. Both code samples need to be located in a directory that has all other authentication except windows authentication turned off. I’ve posted a downloadable copy of both examples here the second example looks like.

<%@ Page %>
<%@Import namespace="System.Data"%>
<%@Import namespace="System.Net"%>
<%@Assembly name="System.DirectoryServices, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a, Custom=null"%>
<%@Import namespace="System.DirectoryServices"%>
<%@Import namespace="System.Security.Cryptography.X509Certificates"%>
<script language="vb" runat="server">

Public Class AcceptAllCertificatePolicy
Implements ICertificatePolicy
Public Overridable Function CheckValidationResult(ByVal srvPoint As ServicePoint,
ByVal certificate As X509Certificate, ByVal request As WebRequest, ByVal problem
As Integer) As Boolean Implements ICertificatePolicy.CheckValidationResult
Return True 'this accepts all certificates
End Function
End Class

Sub Page_Load(sender As Object, e As EventArgs)
System.Net.ServicePointManager.CertificatePolicy = New
Dim Request As System.Net.HttpWebRequest
Dim Response As System.Net.HttpWebResponse
Dim strRootURI As String
Dim strQuery As String
Dim bytes() As Byte
Dim workrow As System.Data.DataRow
Dim resrow As System.Data.DataRow
Dim impersonationContext As
Dim currentWindowsIdentity As System.Security.Principal.WindowsIdentity
currentWindowsIdentity = CType(User.Identity,
impersonationContext = currentWindowsIdentity.Impersonate()
Dim MyCredentialCache As System.Net.CredentialCache
Dim resdataset As New System.Data.DataSet
Dim RequestStream As System.IO.Stream
Dim ResponseStream As System.IO.Stream
Dim ResponseXmlDoc As System.Xml.XmlDocument
Dim DisplayNameNodes As System.Xml.XmlNodeList
Dim objsearch As New System.DirectoryServices.DirectorySearcher
Dim strrootdse As String = objsearch.SearchRoot.Path
Dim objdirentry As New system.DirectoryServices.DirectoryEntry(strrootdse)
Dim objresult As system.DirectoryServices.SearchResult
Dim stremailaddress As String
Dim strhomeserver As String
objsearch.Filter = "(&(&(&(& (mailnickname=*) (|
(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)" _
& "(msExchHomeServerName=*)))
)))(objectCategory=user)(userPrincipalName=*)(mailNickname=" &
System.Environment.UserName & ")))"
objsearch.SearchScope = DirectoryServices.SearchScope.Subtree
objsearch.Sort.Direction = DirectoryServices.SortDirection.Ascending
objsearch.Sort.PropertyName = "mail"
Dim colresults As DirectoryServices.SearchResultCollection = objsearch.FindAll()
For Each objresult In colresults
stremailaddress = objresult.GetDirectoryEntry().Properties("mail").Value
strhomeserver =
Dim emailNameNodes As System.Xml.XmlNodeList
strhomeserver = Right(strhomeserver, Len(strhomeserver) - (InStr(strhomeserver,
"cn=Servers/cn=") + 13))
strRootURI = "https://" & strhomeserver & "/exchange/" & stremailaddress &
strQuery = "<?xml version=""1.0""?>" & _
"<D:searchrequest xmlns:D = ""DAV:"" >" & _
"<D:sql>SELECT ""urn:schemas:contacts:cn"",
"""" " & _
"FROM """ & strRootURI & """" & _
"WHERE ""DAV:ishidden"" = false AND ""DAV:isfolder"" = false AND
""DAV:contentclass"" = 'urn:content-classes:person'" & _
Request = CType(System.Net.WebRequest.Create(strRootURI), _
Request.Credentials = System.Net.CredentialCache.DefaultCredentials
Request.Method = "SEARCH"
bytes = System.Text.Encoding.UTF8.GetBytes(strQuery)
Request.ContentLength = bytes.Length
RequestStream = Request.GetRequestStream()
RequestStream.Write(bytes, 0, bytes.Length)
Request.ContentType = "text/xml"
Request.Headers.Add("Translate", "F")
Response = CType(Request.GetResponse(), System.Net.HttpWebResponse)
ResponseStream = Response.GetResponseStream()
ResponseXmlDoc = New System.Xml.XmlDocument
DisplayNameNodes = ResponseXmlDoc.GetElementsByTagName("d:cn")
emailNameNodes = ResponseXmlDoc.GetElementsByTagName("e:email1emailaddress")
Dim resultstable As DataTable
resultstable=new DataTable()
resultstable.Columns.Add("Contact Name")
resultstable.Columns.Add("Email Address")
AddRow(resultstable, "", "Tests")
If DisplayNameNodes.Count > 0 Then
Dim i As Integer
For i = 0 To DisplayNameNodes.Count - 1
AddRow(resultstable, emailNameNodes(i).InnerText, DisplayNameNodes(i).InnerText)
Console.WriteLine("No non-folder items found...")
End If
End Sub

Sub AddRow(resultstable As DataTable, Emailaddress As String, Name As String)
Dim row As DataRow
row("Contact Name")=Name
row("Email Address")=Emailaddress
End Sub
<form id="Recipe1416vb" method="post" runat="server">
<asp:DataGrid ID="Showcontacts" Runat="server" AutoGenerateColumns="False">
<asp:BoundColumn DataField="Contact Name" HeaderText="Contact Name" />
<asp:BoundColumn DataField="Email Address" HeaderText="Email Address" />

Querying the MicrosoftExchangeV2 namespace remotely in WMI without admin rights

A few people asked about this today so I thought I'd share this with everyone

Be default the Exchange WMI namespace root/MicrosoftExchangeV2 is only query-able remotely by Administrators because of the default security that is applied to it. If you need to query this namespace and any of the classes within it remotely using a user other then a administrator what you need to do is change the permissions on the root/MicrosoftExchangeV2 object so that this particular user has the Remote enable right.This is a per server thing so needs to be done on every server that you want these users to have this access. Giving a user Remote enable right gives them the right to connect to the namespace and issue a query but to actually return data from any of the class's like Exchange_Mailbox the user will still require View only Exchange Admin rights.

Before you make any changes you should consider the security implications around doing this (eg your server is now less secure then it was before because you've given rights to a user to do something that couldn't be done previous although you have gained some functionality out of doing so).

Some Doco on modifying WMI namespace security can be found here and here

Wednesday, April 13, 2005

Assigning the open address list permission on a Address list with ADSI

Somebody asked me what the ACE access mask is for Open Address List on an address list object in Exchange. I thought this should be a relatively straight forward thing to find but apparently not although most of the other ADSI permission enums are documented and used a fair bit this one seems to be a little neglected. There are some hidden gems in the Exchange SDK but unless you know what you looking at its easy to look straight over them. The SDK gives us a create GAL sample using ADSI . Within this sample the following line gives the accessmask and all the right setting needed to add an “open address list ACE”

AddAce objCopyDACL, szUserGroup + "@" + szDomain, 256, 5, 2, 1, "{A1990816-4298-11D1-ADE2-00C04FD8D5CD}", 0

Unfortunately they haven’t used a constant in this script to tell you what the 256 access mask means but there is an easy way I use to work this out. Which is basically add a unique user to an address list and give it only the “Open address list” right and then use the following little script to output what the ACE entries are for that object. Then all I need to do is look at what access mask gets assigned to the ACE for the unique user I added. The script to do is a simple ACE list example which looks like. This is a modified version of the script from this kb

sUserADsPath = "LDAP://DN-of-Addresslist"
Set objadlist = GetObject(sUserADsPath)
Set oSecurityDescriptor = objadlist.Get("ntSecurityDescriptor")
Set dacl = oSecurityDescriptor.DiscretionaryAcl
Set ace = CreateObject("AccessControlEntry")
wscript.echo "Here are the existing ACEs in the DACL:"
For Each ace In dacl
' Display all the properties of the ACEs using the IADsAccessControlEntry interface.
wscript.echo ace.Trustee & ", " & ace.AccessMask & ", " & ace.AceType & ", " & ace.AceFlags & ", " & ace.Flags & ", " & ace.ObjectType & ", " & ace.InheritedObjectType
wscript.echo "Done viewing descriptor"

So to add a user with the open address list right to a existing address list all you need to do is combine bits from the create GAL sample I mentioned above with the addace and reorder ACE function in the Exchange SDK. The result of such a combination looks something like this. I’ve placed a download copy of the scripts here.

strDNofADdresslist = "LDAP://DN-of-Addresslist"
Set objgal = GetObject(strDNofADdresslist)
Set objSecurityDescriptor = objgal.Get("ntSecurityDescriptor")
iCurrentControl = objSecurityDescriptor.Control
objSecurityDescriptor.Control = iCurrentControl Or SE_DACL_PROTECTED
Set objParentSD = objgal.Get("ntSecurityDescriptor")
Set objParentDACL = objParentSD.DiscretionaryAcl
Set objCopyDACL = objParentDACL.CopyAccessList()

AddAce objCopyDACL, "domain\userID", 256, 5, 2, 1, "{A1990816-4298-11D1-ADE2-00C04FD8D5CD}", 0

Set objNewDACL = ReorderACL(objCopyDACL)
objSecurityDescriptor.DiscretionaryAcl = objNewDACL
objgal.Put "ntSecurityDescriptor", objSecurityDescriptor

Function AddAce( objDacl, _
szTrusteeName, _
gAccessMask, _
gAceType, _
gAceFlags, _
gFlags, _
gObjectType, _
Set Ace1 = CreateObject("AccessControlEntry")
Ace1.AccessMask = gAccessMask
Ace1.AceType = gAceType
Ace1.AceFlags = gAceFlags
Ace1.Flags = gFlags
Ace1.Trustee = szTrusteeName
If CStr(gObjectType) <> "0" Then
Ace1.ObjectType = gObjectType
End If
If CStr(gInheritedObjectType) <> "0" Then
Ace1.InheritedObjectType = gInheritedObjectType
End If
objDacl.AddAce Ace1
Set Ace1 = Nothing
wscript.echo "Ace added"
End Function

Function ReorderACL(objDacl)
' Set Constants.
Set objSD = CreateObject("SecurityDescriptor")
Set newDACL = CreateObject("AccessControlList")
Set ImpDenyDacl = CreateObject("AccessControlList")
Set ImpDenyObjectDacl = CreateObject("AccessControlList")
Set ImpAllowDacl = CreateObject("AccessControlList")
Set ImpAllowObjectDacl = CreateObject("AccessControlList")

For Each ace In objDacl
Select Case ace.AceType
ImpDenyDacl.AddAce ace
ImpDenyObjectDacl.AddAce ace
ImpAllowDacl.AddAce ace
ImpAllowObjectDacl.AddAce ace
Case Else
wscript.echo "bad ACE"
End Select
' Combine the ACEs in the Proper Order
' Implicit Deny.
For Each ace In ImpDenyDacl
newDACL.AddAce ace
' Implicit Deny Object.
For Each ace In ImpDenyObjectDacl
newDACL.AddAce ace
' Implicit Allow.
For Each ace In ImpAllowDacl
newDACL.AddAce ace
' Implicit Allow Object.
For Each ace In ImpAllowObjectDacl
newDACL.AddAce ace
newDACL.AclRevision = objDacl.AclRevision
Set ReorderACL = newDACL
Set newDACL = Nothing
Set ImpAllowObjectDacl = Nothing
Set ImpAllowDacl = Nothing
Set ImpDenyObjectDacl = Nothing
Set ImpDenyDacl = Nothing
Set objSD = Nothing
wscript.echo "DACL Reordered"
End Function

Wednesday, April 06, 2005

Modify Public Folder custom attributes via script and .Net

Somebody asked today about a script to change the custom attributes of a public folder on Exchange 2x. I was supprised that i couldn't find any samples out there to do this (maybe i didn't look hard enough) so I thought i'd bolt one together. Its some pretty simple ADSI code just uses one LDAP filter to find the public folder's DN based on the public folders emailaddress (its about the only real unique property you can search on) and then connects to the folder and makes the mod using ADSI. Its designed to run with two commandline parameters the first is the email address of the public folder and the second is the value for the custom attribute. The script modifies extensionAttribute1 but its can be easly adapted to modify other attributes. I've also included a VB.NET sample that does the same thing using System.Directoryservices just for fun.

I've posted a downloadable copy of both sample here

The script looks like

pfnamemail = wscript.arguments(0)
customvalue = wscript.arguments(1)
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("defaultNamingContext")
rangeStep = 999
lowRange = 0
highRange = lowRange + rangeStep
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
pfQuery = ";(&(&(&(& (mailnickname=*) (| (objectCategory=publicFolder) )))(objectCategory=publicFolder)(mail=" & pfnamemail & ")));name,distinguishedName;subtree"
Com.ActiveConnection = Conn
Com.CommandText = pfQuery
Set Rs = Com.Execute
While Not Rs.EOF
set objuser = getobject("LDAP://" & rs.fields("distinguishedName"))
objuser.extensionAttribute1 = customvalue
wscript.echo "Modified folder " & objuser.displayname & " Added attribute " & objuser.extensionAttribute1

The .NET sample looks like

Dim strnewvalue As String = "newvalue"
Dim strpfnamemail As String = ""
Dim objsearch As New System.DirectoryServices.DirectorySearcher
Dim strrootdse As String = objsearch.SearchRoot.Path
Dim objdirentry As New System.DirectoryServices.DirectoryEntry(strrootdse)
Dim objresult As System.DirectoryServices.SearchResult
Dim stremailaddress As String
Dim strhomeserver As String
Dim pffolder As System.DirectoryServices.DirectoryEntry
Dim pffolderdn As String
objsearch.Filter = "(&(&(&(& (mailnickname=*) (| (objectCategory=publicFolder) )))(objectCategory=publicFolder)(mail=" & strpfnamemail & ")))"
objsearch.SearchScope = DirectoryServices.SearchScope.Subtree
Dim colresults As DirectoryServices.SearchResultCollection = objsearch.FindAll()
For Each objresult In colresults
pffolderdn = "LDAP://" & objresult.GetDirectoryEntry().Properties("distinguishedName").Value
pffolder = New System.DirectoryServices.DirectoryEntry(pffolderdn)
pffolder.Properties("extensionAttribute1").Item(0) = strnewvalue