SSH with Powershell

SSH with Powershell

Recently I went over how to get information from the Unifi Controller. Now I want to show you how to grab information from the unifi device itself. In this example, we are going to use SSH with Powershell to pull some basic information from an unifi AP.

SSH with Powershell

The first thing you will need is to make an ssh connection. Most computers have open ssh installed and you can type in ssh username@ipaddress and access it after inputting passwords and such. However, I want to work with powershell and not whatever shell the unifi will present me. Thus, we will run through a module called POSH-SSH by darkoperator.

Install Posh-ssh

The first thing you will need to do, if you haven’t already, is installing the module. This module lives in the standard repos, which makes life so much easier. Here is the command:

Install-Module -Name Posh-SSH

Once you have the command installed, that’s when we import with the import module command.

Making a connection

The next step is to make the connection. The feel of this command is just the same as that of enter-pssession. We call the computer name, normally an IP address, and we add the credentials. The biggest difference is we add an acceptkey flag. You can also give your session an ID number, this is good if you are doing more than one at a time. Today we are only focusing on one connection. So it’s not needed, but it becomes very useful with loops. Like always, these sessions start at 0.

New-SSHSession -ComputerName 10.0.0.8 -AcceptKey -Credential (Get-Credential)

This creates a session of 0 which we can send commands to later. Remember, each session connection adds another session number. So the next one will be 1 and so on so forth. if you need more information or become lost with which sessions you are using, get-sshsession will help resolve this question.

Invoking SSH commands with Powershell

The next thing we need to do is invoke commands. What is the point of just connecting when we have the power of ssh and powershell? The command we can use is called invoke-sshcommand. When working with any system, you need to be mindful of that system’s shell. Ssh gives you the connection, and that system determines what you can use. This took me a while to figure out. One of the commands inside the unifi devices is info. This gives you useful information. So when you are connected, typing info will telling you IP addresses, mac address, inform statuses, and more. However, with the SSHcommand, it produces nothing useful. As the image below shows.

So, what is needed to do from here is adding a flag to let it know you are coming from somewhere else. This flag is “mca-cli-op” followed by the command you want. However, as the image below suggests, it parses the output as a single object. We need to go deeper by selecting the output.

$Output = (Invoke-SSHCommand -Command "mca-cli-op info" -SessionId 0).output

This is nice to know, however… It’s a string. You will need to parse this data out accordingly. I do this by using select-string then from there working each item. When I select the string, for example, model. I take the output and select-string model. Then i convert that to a string as select string make it into an match info object. Which isn’t as helpful as you would think. Then from there, I replace any spaces and split from the : to get the information I need. Below is the code.

$Return = [PSCustomObject]@{
        model = ($output | select-string model).tostring().replace(' ','').split(":")[1]
        version = ($output | select-string version).tostring().replace(' ','').split(":")[1]
        MacAddress = ($output | select-string mac).tostring().split(' ')[-1]
        IPaddress = ($output | select-string ip).tostring().replace(' ','').split(":")[1]
        Hostname = ($output | select-string hostname).tostring().replace(' ','').split(":")[1]
        UptimeSeconds = ($output | select-string uptime).tostring().replace(' ','').split(":")[1] -replace("\D",'')
        Status = ($output | select-string status).tostring().split(':').split('(').replace(' ','')[1]
        Inform = ($output | select-string status).tostring().split(' ')[-1] -replace('[()]','')
    }

From here you can change the inform, or other items using the flag and the command accordingly. Once you are finished with your session, make sure to remove it with the Remove-SSHSession command.

The Script

Here is the script. This script allows you to do more than one IP address.

function Get-UnifiSSHDeviceInformation {
    param (
        [parameter(
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True,    
            HelpMessage = "Unifi Username and Password", 
            Mandatory = $true)][alias('IP')][ipaddress[]]$IPaddress,
        [parameter( 
            HelpMessage = "Unifi Username and Password", 
            Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential
    )
    $return = @()
    foreach ($IP in $IPaddress) {
        New-SSHSession -ComputerName $IP -AcceptKey -Credential $Credential
        $Output = (Invoke-SSHCommand -Command "mca-cli-op info" -SessionId 0).output
        Remove-SSHSession -SessionId 0
        $Return += [PSCustomObject]@{
            model = ($output | select-string model).tostring().replace(' ','').split(":")[1]
            version = ($output | select-string version).tostring().replace(' ','').split(":")[1]
            MacAddress = ($output | select-string mac).tostring().split(' ')[-1]
            IPaddress = ($output | select-string ip).tostring().replace(' ','').split(":")[1]
            Hostname = ($output | select-string hostname).tostring().replace(' ','').split(":")[1]
            UptimeSeconds = ($output | select-string uptime).tostring().replace(' ','').split(":")[1] -replace("\D",'')
            Status = ($output | select-string status).tostring().split(':').split('(').replace(' ','')[1]
            Inform = ($output | select-string status).tostring().split(' ')[-1] -replace('[()]','')
        }
    }
    $Return
}

Additional Reading

Get-Credentials to Plain Text

Get-Credentials to Plain Text

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.

 $CodedPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)

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.

$Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($CodedPassword)

The Script Rebuilt

Now we have a way to decrpt the get credential command, it’s time to dive back into the script. Here is the rebuilt script.

function Get-UnifiSitesClients {
        param (
            [parameter(
                ValueFromPipeline = $True,
                ValueFromPipelineByPropertyName = $True,    
                HelpMessage = "Unifi Username and Password", 
                Mandatory = $true)][alias('IP')][ipaddress]$IPaddress,
            [parameter( 
                HelpMessage = "Unifi Username and Password", 
                Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential,
            [parameter(
                HelpMessage = "Port Number", 
                Mandatory = $true)][alias('Port')][init]$portNumber
        )
        $SecurePassword = $Credential.Password
        $CodedPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)
        $Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($CodedPassword)
        $Username = $Credential.UserName

        $uri = "https://$($IPaddress):$portNumber/api/login"
        $headers = @{'Accept' = 'application/json' }
        $params = @{
            'username' = $username;
            'password' = $Password;
        }
        $body = $params | ConvertTo-Json
     
        $response = Invoke-RestMethod -Uri $uri `
            -Body $body `
            -Method Post `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -SessionVariable s
        $uri = "https://$($IPaddress):$portNumber/api/self/sites"
        $sites = Invoke-RestMethod -Uri $uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s
        $Return = Foreach ($Site in $sites.data) {
            $Uri = "https://$($IPaddress):$portNumber/api/s/$($Site.name)/stat/sta"
            $Clients = (Invoke-RestMethod -Uri $Uri `
                -Method Get `
                -ContentType 'application/json' `
                -Headers $headers `
                -SkipCertificateCheck `
                -Websession $s).data
            Foreach ($Client in $Clients) {
                [pscustomobject][ordered]@{
                    Site = $Site.name
                    SiteDescritption = $Site.desc
                    OUI = $client.OUI
                    MacAddress = $client.mac 
                    IPAddress = $Client.IP
                    SwitchMac = $client.sw_mac
                    SwitchPort = $client.sw_port
                    WireRate = $client.wired_rate_mbps
                }
            }
        }
        $return
    }

Image created with mid-journey

Troubleshooting Radius – IP Changes

Troubleshooting Radius – IP Changes

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.

  1. Changed all the Access Points to Static instead of DHCP
  2. 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.

Additional Reading:

Get Client Information From your Unifi Controller

Get Client Information From your Unifi Controller

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.

$params = @{
        'username' = $username;
        'password' = $Password;
}
$body = $params | ConvertTo-Json

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.

$response = Invoke-RestMethod -Uri $uri `
        -Body $body `
        -Method Post `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -SessionVariable s

Grabbing Sites

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.

$uri = "https://$($IPaddress):8443/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.

$sites = Invoke-RestMethod -Uri $uri `
        -Method Get `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -Websession $s

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.

$Uri = "https://$($IPaddress):8443/api/s/$($Site.name)/stat/sta"

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.

(Invoke-RestMethod -Uri $Uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s).data

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.

function Get-UnifiSitesClients {
    param (
        [string]$IPaddress,
        [string]$username,
        [string]$portNumber,
        [string]$Password
    )
    
    $uri = "https://$($IPaddress):$portNumber/api/login"
    $headers = @{'Accept' = 'application/json' }
    $params = @{
        'username' = $username;
        'password' = $Password;
    }
    $body = $params | ConvertTo-Json

    $response = Invoke-RestMethod -Uri $uri `
        -Body $body `
        -Method Post `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -SessionVariable s
    $uri = "https://$($IPaddress):$portNumber/api/self/sites"
    $sites = Invoke-RestMethod -Uri $uri `
        -Method Get `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -Websession $s
    $Return = Foreach ($Site in $sites.data) {
        $Uri = "https://$($IPaddress):$portNumber/api/s/$($Site.name)/stat/sta"
        $Clients = (Invoke-RestMethod -Uri $Uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s).data
        Foreach ($Client in $Clients) {
            [pscustomobject][ordered]@{
                Site = $Site.name
                SiteDescritption = $Site.desc
                OUI = $client.OUI
                MacAddress = $client.mac 
                IPAddress = $Client.IP
                SwitchMac = $client.sw_mac
                SwitchPort = $client.sw_port
                WireRate = $client.wired_rate_mbps
            }
        }
    }
    $return 
}

Additional Resource