Writing a Active Directory Audit Module - Getting a DirectoryEntry
Extending the information
In the previous blog post when we look at the object returned it has all of the information properly parsed and shown so I do not have to run around parsing fields and converting them but for me a critical piece of information is not shown and that is the SID of the forest domain. If you have played with analysis of some logs and with Mimikatz attacks you know the SID is of great importance. For this we will use the System.DirectoryServices namespace, specifically the DirecotryEntry class that represents a path in AD.
Designing Get-DSDirectoryEntry
We will create a helper function to generate the DirectoryEntry object, by creating the function we ensure we do not duplicate a lot of code unless we have to and will also make it easier to test.
Before we start coding lets define what we want to achieve and this is dictated in part by the APIs we want to use. in this case the Class has several constructors to create an instance of it:
We want to be able to get a DirectoryEntry int he following manners:
- For a specified path using the current user credentials.
- For a specified path using alternated credentials.
- For a specified path by connecting to a server and providing credentials
Now that we know what we want we can start creating the skeleton of the script and writing the test. We start by using Pester New-Fixture cmdlet to create the script and test file. We will give it a name of Get-DSDirectoryEntry.
When we look at the requirements for the first one PowerShell provides what is called an accelerator, in this case it is names [ADSI] and it takes a string that represent the path in AD in LDAP 509 format, but we will include it in the function so as to be complete.
We create on our script the parameter sets.
We can test the parameter sets by dot sourcing the script and look at the parameter sets under Syntax
Writing out Test Script
For our test script we can even use the same cmdlet attribute and the same parameter and sets if we do not want to parse each of the parameter by hand and just like with the forest test and script we want to check if the machine is domain joined or not.
Under the Describe block we add the switch statement for each test depending the parameters used and we write the It statements with the tests for each.
Tests are simply executing the function, saving the value in to a variable and then checkin the type returned if it is the correct type then the test passed. In the case of where we check for a exception we execute a it inside a script block to see if it throws one but we check the message returned to make sure it is the correct one.
The code follows the same logic used in the test we wore for Get-DSForest in the previous blogpost.
Tests should fail since we have not written any code in our function but we can run it just to make sure the base logic of the test is working properly.
Writing the Function
Now that we have test al written we can start writing the function and testing the function as we complete parts of it. When we look at the constructors we selected all take a "Path" to the directory entry we want to create a representation of. This is in the form of a <resource>://<server>:<port>/<distinguishedname> where the server and port is optional. As a resource one normally uses LDAP all in uppercase since it is case sensitive and the server and port are optional. Examples bellow
Once we know how to write the path for a local connection, remote one to a specified server and one to a specific distinguished name it becomes easy to build the object we want. Example bellow connecting to specific path with alternate credentials.
Lets start with the coding of the function it self. When we look at a PowerShell Advanced function we see a begin{}, process{} and end{} script blocks. What they mean is that when the function is ran inside a pipeline the code in the begin block will run once for the life of the pipeline, the process block for each object that passes the pipeline and end runs well at the end of the full execution of the pipeline. Knowing this I want the code that checks if the local host is in a domain or not to run only once so I place the same code we used in the previous function and in the tests inside this block.
In the process block we will add the logic for generating our objects. We will use just like in the test a switch statement against the ParameterSetName. We will start with generating the object when there is no other parameter or what we called the "Current" parameter set. in it we check the status and of the machine is joined variable and if joined we use the accelerator with path provided in the parameter.
For the a remote connection we use the constructor as we discussed above where we create a LDAP path with the server name and then the distinguished name. We use the path, username and password from the credential object to create the object.
In the case of an alternate credential we follow the same logic as above with the only difference being that we do not put the ComputerName in the path to the object being selected.
One of the the quick improvements I though of as testing the function is how I can make it be more flexible when piping information from other sources. So I made the DistinguishedName parameter one that can accept values from the pipeline by type (String) and by property name. Bellow the modification of the parameter.
If we look at an object for the Users group, in this case I'm creating the object by giving a path using a well know SID we can see that the members of the group is a collection of distinguished names, so we can use this to test the parameter.
Using the variable where we stored the group we pipe the content of the property "members" to the function and since the only parameter that accepts a single string at a time is DistinguishedName it gets mapped to it and we get a full object for each.
Extending Get-DsForest
Now that we have our function ready we can use it to get an instance of the root domain so we get the SID and add it as a member of the object we are returning. The logic would be as follows:
- Check if we got an object back and the connection did not failed.
- Using the forest name we turn the FQDN in to a distinguished name by manipulating the string replacing the dots with ",DC=" and appending a "DC=" to the start of the string.
- We use the Get-DSDirectoryEntry function to get an instance of the object using the created distinguished name.
- We convert the byte array that is the objectSid property in to a string representation of the SID by creating a SecurityIdentifier object using the byte array as an argument.
- We use the Add-Member cmdlet to add the Sid property to the exiting object.
For the current parameter set we add the block of code with the logic we discussed above and we use the Get-DSDirectoryEntry function with only the DistiguishedName parameter since it is not a remote connection.
For the remote parameter set we use the exact same logic but we just add the ComputerName and Credential parameter to the Get-DSDirectoryEntry function
For the parameter set that gets the forest though a trust relationship and also permits the use of alternate credentials we must take in to account the same logic used to create the object where we check if the credential object has an actual username in it or not since it is an empty object when not specified so as to create the proper object.
We use the same logic and pass the proper parameter based on it to the Get-DSDirectoryEntry function
We can test the function now and we see that it has a Sid property now.
Once we are sure everything is working properly we can add the help comment block for the function.
Tying Everything Together in to the Module
Now that we have finished the function we just tie it in to the module so it is loaded properly. We start by dot sourcing it in the main module. You will notice that I'm using now $PSScriptRoot dynamic variable since it is a safer way to specify the path and a relative one since it will give me the full path of the module being loaded.
Once the function is configured so it will be sourced we can now export it outside of the module by modifying the FunctionToExport array in the manifest.
To test it is working properly we use the Import-Module cmdlet and we give it the path to the folder where the module is and the Verbose and Force parameter to force a reload of the module if already loaded and see if the function is exported.
As always I hope you found the blog post useful.