While working with Group Policy this month, I ran across an oddity I have never seen before, “Extra Registry Settings” These were inside the default domain policy and I had no idea why they were there. So, I did some research.
What are they?
These keys come from when you have had an ADM or an ADMX installed. Then you create a policy. Later down the road, the ADM or ADMX is removed. These keys will be left behind. So, it’s a tattoo. More like a tattoo on the tattoo artist. You can read more about GPO tattooing here. The main difference between a normal tattoo and this tattoo is one happens on the end user’s computer, while the other happens on both the Group Policy and the end user’s computer.
This is one of those things that can go Very deep. To keep things simple, I will keep it general.
How do you remove it?
Inside the Group Policy Management Module that is installed with Rstat, you have a remove-GPRegistryValue command that can do what you are looking for. You will need the following, The Name of the policy and the key. Please note, these keys live in the HKEY_Local_Machine. Thus, you will need to add such to the command. Your registry might live in the Current users. Do a little research. Here is the code that I used to remove these two items:
Once you remove the Extra Registry keys, you can reload your Group Policy Management and remove the policy. Or you can replace Remove with Get. This will try to get the value, but if it doesn’t exist anymore, PowerShell will present an error.
Sometimes I like to go back and see where I can improve my scripts. A few weeks ago I showed you the Unifi Controller script. Where it pulls the client information from each site. Well, I wanted to improve upon this script. A problem I see with this script is the plain text password. The unifi controller I have to test only accepts plain text, but I don’t want to have a password inside my PowerShell history. Today I want to show you how to convert Get-Credentials to Plain Text.
Get-Credentials as a Parameter
We first need to remove the Username and Password from the parameters and then create a secure credential parameter. By default, we want this to be a mandatory parameter. The type of parameter we will be using is a System Management Automation PScredential. This is what the parameter will look like.
[parameter(
HelpMessage = "Unifi Username and Password",
Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential
Convert Get-Credentials to Plain Text
Now it’s time to Covert the PSCredentials back to plain text. Inside our PSCredentials lives the username and password we gave the system. Thus we pull the secure string password from the PSCredentials.
$SecurePassword = $Credential.Password
Next, we are going to use the Marshal Class. This class is designed to bridge the gap between managed and unmanaged programming models. The method we will be using is the Secure String to BSTR. This method Allocates an unmanaged binary string and copies the content of a managed secure string object into it. Basically, we find where it lives in memory and combine it. For more information.
Aftward, we Take this coded data and push it into the following method. The PtrToStringAuto. This method allocates a managed string and copies all the characters up to the first null. This copy is from the string stored in unmanaged memory. This is why we need the Coded Password to tell us where the password lives in memory. Basically, I’m taking the coded password and making it human-readable.
A few of my clients use something called LAPS. Laps change the local administrator password on a computer and then store the information inside Active Directory. Since I don’t dive deep into this client’s computers often, I needed something to quickly type the first letter of the computer in question to pull up the LAPS password. Basically, I needed a list of computer names from the command itself. This is fully possible with Dynamic Parameters. So, today we will be grabbing the LAPS password with PowerShell.
Where Does LAPS Password live?
Most companies that set up LAPS do so with Active Directory. By default, Active Directory saves the password into an attribute called “ms-Mcs-AdmPwd” and LAPS also stores the expiration date in “ms-Mcs-AdmPwdExpirationTime” Thus, all you have to do is call a get-adcomputer command and pull out the information.
Now the “ms-Mcs-AdmPwdExpirationTime” is unique and needs to be parsed into something more readable. We can use the m method called [datetime] to do this.
There you have it, That’s how you get the LAPS password, But I want to take this one step further. I don’t know all the computer names. I want that information at my fingertips while I type out the command. So, I want to type something like Get-LAPS -ComputerName <here populate a name where I can tab> and bamn, it gives it to me when I hit enter. That’s where we will dive into dynamic parameters next.
Adding Dynamic Computer Name Parameters
In a previous article, we went over how to make a dynamic parameter. I want to help refresh memories by doing a single parameter and show you how it can be done with this function.
The first thing we need to do is create our form. This form allows us to use the dynamic parameters and gives us spots to pull data. This is more of a road map than anything else, but it is required for dynamics.
function Get-LapsPassword {
[cmdletbinding()]
Param()
DynamicParam {}
Begin {}
Process {}
End {}
}
The first part of our Dynamics is we want to name the parameter template. From there, we want to create a new object. This object will be the system collections object model for collections. AKA system attributes. Then we want to make an attribute object to add to that object later.
The ParameterAttribute will be where we add the flags like mandatory and position. We add those by dropping them directly into the ParamterAttribute object. A fun little fact, you can tab through and see what other items are available for this object. Things like the help message, the value from the pipeline, and more are hidden here. Today we only care about the mandatory and position.
After we build out our parameter Attribute object, we need to add it to the Attribute Collection we made at the start. We do this by using the “.add()” function of objects.
$AttributeCollection.Add($ParameterAttribute)
Now we need to create another object. This will be the Runtime Parameter Directory. Basically, what they will be looking through. This is a system management automation object called runtime defined parameter directory. Say that 10 times fast…
Now we need to make our Validate Set. We will create an array of devices using the Get-adcomputer command. Here we will push (Get-adcomputer -filter {enabled -eq “true”}).name into a variable. Now we will have a list of active computers. Notice that we filter out all other information by using the “.name” call.
Next, we need to create another object. This object is the system management automation validate set attribute object. We can feed this object our Parameter Validate Set.
Afterward, it’s time to feed the Validate Set attribute to the attribute collection from the beginning. We can accomplish this by using the “.add()” method.
$AttributeCollection.Add($ValidateSetAttribute)
Next, it’s time to bring our Attribute collection into the command line. It’s time to make the run-time parameter. Once again, a new object. This time it’s the Run time Defined Parameter object. Like the last object, we can place our data directly into it. We will want the parameter’s name, the type, a string in this case, and the validate set.
Afterward, we take the above parameter and place it into our directory with the “.add()” method. We need the parameter Template and the Run time Parameter.
Finally, in the dynamic parameter block, we return our directory.
return $RuntimeParameterDictionary
Beginning
We are almost done. It’s time to bring the dynamic parameter into the function and make it useable. We do this in the beginning section. We shove the PSBoundParameters of our template name into a variable.
$MemberName = $PSBoundParameters[$paramTemplate]
Then from there, we call the $memberName in our Get-adcomputer command.
The Script
It’s that time, it’s time to put it all together, so you can copy and past it into your toolbox. It’s time To Grab LAPS Password With PowerShell.
function Get-LapsPassword {
[cmdletbinding()]
Param()
DynamicParam {
# Need dynamic parameters for Template, Storage, Project Type
# Set the dynamic parameters' name
$paramTemplate = 'ComputerName'
# Create the collection of attributes
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
# Create and set the parameters' attributes
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
# Add the attributes to the attributes collection
$AttributeCollection.Add($ParameterAttribute)
# Create the dictionary
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Generate and set the ValidateSet
$ParameterValidateSet = (Get-ADComputer -Filter { enabled -eq "true" }).name
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ParameterValidateSet)
# Add the ValidateSet to the attributes collection
$AttributeCollection.Add($ValidateSetAttribute)
# Create and return the dynamic parameter
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($paramTemplate, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($paramTemplate, $RuntimeParameter)
return $RuntimeParameterDictionary
} # end DynamicParam
BEGIN {
$MemberName = $PSBoundParameters[$paramTemplate]
} # end BEGIN
Process {
$ComputerInfo = Get-ADComputer -Filter { name -like $MemberName } -Properties *
}
End {
$ComputerInfo | select-object Name, @{l = "AdminPassword"; e = { $_."ms-Mcs-AdmPwd" } }, @{l = "AdminPasswordExpireTime"; e = { [datetime]::FromFileTime(($_."ms-Mcs-AdmPwdExpirationTime")) } }
}
}
Last October, I ran across a client with a broken radius. I want to go through the process I used to troubleshoot the issue. The goal of this to bring you a level of understanding of the troubleshooting processes. Not every process is the same for each It related item. Getting exposed to different steps from different people helps out.
Scenario
Here is the scenario. The client called and stated that no one is able to connect to the wifi. I looked at the device and saw that they were connecting via Radius. Radius allows you to use your username and password for the domain to login into the wifi. It’s one of the more secure ways to setup wifi. I had no documentation to fall back on. Thus, I knew nothing about the radius setup. However, I did know about the wifi controller. It was an Unifi controller.
Troubleshooting Radius – Discovery
Since we know that the devices are connecting to the wifi that is controlled through the Unifi controller, the first logical step is to go to the Unifi controller. I logged into the Controller and went to the settings button at the bottom of the left-hand side of the menu. From there I clicked on the WiFi menu option. I want to look at the wifi profile of the Corporate wifi. The one they are trying to connect to. Next, I scrolled down to the Security area.
Under the Security area, You will see the Radius Profile. Take note of this name. We will call our bob. Once you have that name, Click the profile on the left-hand side of the screen.
At this point, we have discovered the Radius Profile Name. Next, we need to dig into the Profile itself. More information the better when troubleshooting radius. Once you click on the Profile, scroll down to the Radius Section. From here, I found the name of the profile from before and clicked it.
Here we could see the Authentication servers’ IP addresses and ports. Now we know which server Radius is living on. From here, I go to the devices and find the Device they are trying to connect to. Thankfully, the device was named correctly. If it isn’t, then that’s a whole other ball game. I noted the IP address and mac address of the device. The device was active with no connections.
Troubleshooting Radius on the Server
I used RDP to access the IP address with success. I am thankful because sometimes the radius can be setup using compliance of some sort. Next, I connected to the Network Policy Server. After that, I connected to the Radius clients. Looking over the Friendly names, and IP addresses from the Unifi controller and the Radius Server, the problem was clear.
DHCP change occurred on the access points. This meant the NPS radius client IPs were wrong. To correct this, all I have to do is update the NPS Radius client’s IP addresses. However, I don’t want this to happen again. So, here are the steps I took.
Changed all the Access Points to Static instead of DHCP
Change the NPS Radius Client IP addresses to match.
Once I did this, The client was able to reconnect to their wifi using their windows domain credentials.
Today I would like to go over how to get client information from your Unifi Controller. We will first connect to our Unifi Controller with Powershell using the Unifi API. Then from there we will pull each site and create a list of different sites. Finally, we will use the site information to pull the client information from the controller.
Powershell and API the Unifi Controller
The first step is to create a connection with our PowerShell. We will need a few items:
User Account that Has at least read access
The IP address of our controller
The API port.
An Unifi Controller that is configured correctly. (Most are now)
Ask your system administrator to create an account with read access. Check your configuration for the IP addresses and the Port numbers. By default, the API port is 8443. Now we have those pieces of information, its time to discuss what is needed.
Connecting to the API
To connect to the Unifi controller API, you need a Cookie. This is not like how we connected to the Graph API in our previous blog. To get this cookie, we will need to execute a post. The API URL is the IP address followed by the port number, API and login. Very standard.
$uri = "https://$($IPAddress):8443/api/login"
We will need to create a header that accepts jsons. To do this we simply say accept equal application/json.
$headers = @{'Accept' = 'application/json' }
Now we will create our Parameters for the body. Since this is going to be a Post command, we have to have a body. This is where we will place our Username and Password. We are going to build out a Hashtable and then convert that into a json since the API is expecting a json. We will use the convertto-json command to accomplish this step.
Now we have our Body built out, it’s time to create a session. We will use the invoke-restmethod command to do this. We feed the URI we made a few steps back. The body will be the body we just built out. The content type will be ‘application/json’. Our headers will be the header we made a few steps back. We will skip the certificate check unless we are using a cert. Most of the time we are not. Finally, the secret sauce. We want a session. Thus, we use the Session Variable and give it a session name. Let’s go with S for the session.
Now we have our Session cookie, its’ time to grab some information. We will use the same headers but this time we will use the Session Name, S. The URL for sites is /api/Self/Sites.
This URL will get the sites from your Unifi Controller. Once again we will use the Invoke-RestMethod to pull the data down. This time we are using a get method. The same Content type and headers will be used. Along with the skip certificate check. The Session Variable is going to be the Web Session and we will use the $S this time.
The Sites variable will contain two items. a meta and a data variable. Everything we will need is inside the Data variable. To access it we can use the $sites.data to see all the data.
Here is an example of what the data will look like for the sites.
_id : 5a604378614b1b0c6c3ef9a0
desc : A descriptoin
name : Ziipk5tAk
anonymous_id : 25aa6d32-a165-c1d5-73a0-ace01b433c14
role : admin
Get Client Information from your Unifi Controller
Now that we have the sites, we can go one at a time and pull all the clients from that site. We will do this using a Foreach loop. There is something I want to point out first. Remember, the data that is returned is two variables, two arrays. The first one is Meta. Meta is the metadata of the session itself. It’s something we don’t need. We do need the Data. For our foreach loop, we will pull directly from the data array. We want to push everything in this loop into a variable. This way we can parse out useful data, sometime to much data is pointless.
$Clients = Foreach ($Site in $Sites.Data) {
#Do something
}
Without using the $Sites.Data we will have to pull the data from the command itself. This can cause issues later if you want to do more complex things.
The URL we will be using is the /api/s/{site name}/stat/sta. We will be replacing the Site name with our $site.name variable and pushing all that into another Uri.
Then we will execute another invoke-restmethod. Same as before, we use the same headers, content type, and web session. The only difference is we wrap up the command inside preferences. This way we can pull the data directly while the command executes. Less code that way.
Each time the command runs, it will release the data that we need and that will all drop into the $Clients variable. From here, we pull the information we want. The information that the client’s produce includes, the match addresses, times, IP addresses, possible names, IDs and more. So, it’s up to you at this point to pick and choose what you want.
The Script
The final script is different because I wanted to add site names and data to each output. But here is how I built it out. I hope you enjoy it.