Decrypting Remote Desktop Connection Manager Passwords with PowerShell

Today I needed to find a password for a certain account I had used before (but had forgotten), and I remembered that I had stored the credentials in the Remote Desktop Connection Manager, Microsoft’s free RD tool. Of course, it crossed my mind whether these credentials could be decrypted, and it turns out they can, quite easily, with a little PowerShell.

The credentials are stored in encrypted form in the RDG file you create for your RDP connections. It’s just an XML file, so can be easily parsed with PowerShell. Using a handy trick blogged by , I decrypted all the credentials found in the RDG file.

The passwords can only be decrypted with the user profile that added them to the RDG file, however, so they are still reasonably secure. If the credentials were added by another account or on another system, you will get a “Failed to decrypt” error as seen below, which is the same error you get if you try to copy the RDG file and open it on another computer or with another user profile.

Needless to say, I found the password I needed!

capture


# Path to RDCMan.exe
$RDCMan = "C:\Program Files (x86)\Microsoft\Remote Desktop Connection Manager\RDCMan.exe"
# Path to RDG file
$RDGFile = "$env:USERPROFILE\Documents\RDPConnections.rdg"
$TempLocation = "C:\temp"

Copy-Item $RDCMan "$TempLocation\RDCMan.dll"
Import-Module "$TempLocation\RDCMan.dll"
$EncryptionSettings = New-Object -TypeName RdcMan.EncryptionSettings

$XML = New-Object -TypeName XML
$XML.Load($RDGFile)
$logonCredentials = Select-XML -Xml $XML -XPath '//logonCredentials'

$Credentials = New-Object System.Collections.Arraylist
$logonCredentials | foreach {
    [void]$Credentials.Add([pscustomobject]@{
    Username = $_.Node.userName
    Password = $(Try{[RdcMan.Encryption]::DecryptString($_.Node.password, $EncryptionSettings)}Catch{$_.Exception.InnerException.Message})
    Domain = $_.Node.domain
    })
    } | Sort Username

$Credentials | Sort Username

Finding the ‘LastLogon’ Date from all Domain Controllers with PowerShell

In an Active Directory environment, probably the most reliable way to query the last logon time of a computer is to use the Last-Logon attribute.  The Last-Logon-Timestamp attribute could be used, but this will not likely be up-to-date due to the replication lag.  If you are using PowerShell, the LastLogonDate attribute can also be used, however this is also a replicated attribute which suffers from the same delay and potential inaccuracy.

The Last-Logon attribute is not replicated, however, it is only stored on the DC that the computer authenticated against.  If you have multiple domain controllers, you will get multiple values for this attribute depending on which DC the computer has authenticated with and when.

To find the Last-Logon date from the DC that the computer has most recently authenticated with, you need to query all domain controllers for this attribute, then select the most recent.

Following is a PowerShell script I wrote that will read a list of domain controllers from an Active Directory OU, query each one, then return the most recent Last-Logon value.  It uses parallel processing to return the result more quickly than processing each DC in turn, which is useful in a multi-DC environment.

To use the script, simply pass the computer name and optionally the AD OU containing your domain controllers, to the function.  You can hard-code the ‘DomainControllersOU’ parameter in the script if you prefer, so you don’t need to call it.  You need the Active Directory module installed to use this.

Example


Get-ADLastLogon -ComputerName PC001 -DomainControllersOU "OU=Domain Controllers,DC=contoso,DC=com"

capture

Get-ADLastLogon


function Get-ADLastLogon {

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]$DomainControllersOU = "OU=Domain Controllers,DC=contoso,DC=com",

        [string]
        $ComputerName
    )

    # Multithreading function
    function Invoke-InParallel {
        [CmdletBinding()]
        param(
            [parameter(Mandatory = $True,ValueFromPipeline=$true,Position = 0)]
            $InputObject,
            [parameter(Mandatory = $True)]
            [ScriptBlock]$Scriptblock,
            [string]$ComputerName,
            [parameter()]
            $ThrottleLimit = 32,
            [parameter()]
            [switch]$ShowProgress
        )

        Begin
        {
            # Create runspacepool, add code and parameters and invoke Powershell
                [void][runspacefactory]::CreateRunspacePool()
                $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
                $script:RunspacePool = [runspacefactory]::CreateRunspacePool(1,$ThrottleLimit,$SessionState,$host)
                $RunspacePool.Open()

            # Function to start a runspace job
            function Start-RSJob
            {
                param(
                    [parameter(Mandatory = $True,Position = 0)]
                    [ScriptBlock]$Code,
                    [parameter()]
                    $Arguments
                )
                if ($RunspacePool.GetAvailableRunspaces() -eq 0)
                    {
                        do {}
                        Until ($RunspacePool.GetAvailableRunspaces() -ge 1)
                    }

                $PowerShell = [powershell]::Create()
                $PowerShell.runspacepool = $RunspacePool
                [void]$PowerShell.AddScript($Code)
                foreach ($Argument in $Arguments)
                {
                    [void]$PowerShell.AddArgument($Argument)
                }
                $job = $PowerShell.BeginInvoke()

                # Add the job and PS instance to the arraylist
                $temp = '' | Select-Object -Property PowerShell, Job
                $temp.PowerShell = $PowerShell
                $temp.Job = $job
                [void]$Runspaces.Add($temp)  

            }

        # Start a 'timer'
        $Start = Get-Date

        # Define an arraylist to add the runspaces to
        $script:Runspaces = New-Object -TypeName System.Collections.ArrayList
        }

        Process
        {
            # Start an RS job for each computer
            $InputObject | ForEach-Object -Process {
                Start-RSJob -Code $Scriptblock -Arguments $_, $ComputerName
            }
        }

        End
        {
            # Wait for each script to complete
            foreach ($item in $Runspaces)
            {
                do
                {
                }
                until ($item.Job.IsCompleted -eq 'True')
            }

            # Grab the output from each script, and dispose the runspaces
            $return = $Runspaces | ForEach-Object -Process {
                $_.powershell.EndInvoke($_.Job)
                $_.PowerShell.Dispose()
            }
            $Runspaces.clear()
            [void]$RunspacePool.Close()
            [void]$RunspacePool.Dispose

            # Stop the 'timer'
            $End = Get-Date
            $TimeTaken = [math]::Round(($End - $Start).TotalSeconds,2)

            # Return the results
            $return
        }
    }

    # Get list of domain controllers from OU
    try {
    Import-Module ActiveDirectory | out-null
    $DomainControllers = Get-ADComputer -Filter * -SearchBase $DomainControllersOU -Properties Name -ErrorAction Stop | Select -ExpandProperty Name | Sort
    }
    catch {}

    # Define Code to run in each parallel runspace
    $Code = {
        param($DC,$ComputerName)
        Import-Module ActiveDirectory | out-null
        $Date = [datetime]::FromFileTime((Get-ADComputer -Identity $ComputerName -Server $DC -Properties LastLogon | select -ExpandProperty LastLogon))
        $Result = '' | Select 'Domain Controller','Last Logon'
        $Result.'Domain Controller' = $DC
        $Result.'Last Logon' = $Date
        Return $Result
    }

    # Run code in parallel
    $Result = Invoke-InParallel -InputObject $DomainControllers -Scriptblock $Code -ComputerName $ComputerName -ThrottleLimit 64

    # Return most recent logon date
    return $Result | sort 'Last Logon' -Descending | select -First 1
}

 

New Tool: System Explorer for Windows

Today I am pleased to release a new tool for enterprises and home users alike: System Explorer for Windows.  This application can be used to view detailed system and hardware data for a local or remote computer by exposing WMI Win32 classes in an easy-to-use Graphical User Interface.  For enterprises that use System Center Configuration Manager, the application can connect to the SCCM database to allow viewing of hardware inventory data even if the target system is not currently online.

capture
System Explorer for Windows – Client WMI view

Check it out here: https://smsagent.wordpress.com/tools/system-explorer-for-windows/

 

Creating a Simple Class Library for PowerShell 5

PowerShell 5 brings some nice capability to PowerShell, including support for the creation of custom classes.  Very simply, a class can be used to define a custom type, and allow you to create an object of that custom type.  This can be useful for example, if you want to create an object that has specific properties that you define, as well as some methods, or portions of code that do something specific to your need and relevant to that object.

Class libraries are used in programming so that instead of having to create code to do low-level, fundamental or often-repeated tasks, a collection of classes are provided for you so you can simply call a class and its properties and methods when you need it.

You can call .Net classes in Powershell using a type accelerator. For example, this code gives me the value of PI using the System.Math class. It is defined in the class as a static property.


[math]::PI

Since the value of PI is returned with 14 decimal places, I might want to reduce that say to 4 decimal places. So I can call a static method on the System.Math class to round the number of decimal places to 4:


[math]::Round([math]::PI,4)

Create a Custom Class

I won’t go into detail here about how to create a custom class (Stephane van Gulick gives a nice introduction), but below is a simple example of a class I created in PowerShell, which allows me to create an object representing a user account in Active Directory.  I have defined exactly what properties I want to have returned so I don’t have to pass a list of properties to the Get-ADUser cmdlet every time I run it.


class ADUser
{
    [string]$Username
    [string]$Enabled
    [string]$Displayname
    [string]$Title
    [string]$Department
    [string]$EmailAddress
    [string]$CanonicalName
    [string]$City
    [string]$co
    [string]$OfficePhone
    [string]$MobilePhone
    [string]$ipPhone
    [array]$MemberOf
    [array]$DirectReports
    [string]$homeMDB
    [string]$Created
    [string]$Modified
    [string]$LastBadPasswordAttempt
    [string]$PasswordLastSet

   # Constructor
   ADUser ([string] $Username)
   {
        $this.Username = $Username
        $Properties = @(
            'CanonicalName',
            'City',
            'co',
            'Created',
            'Department',
            'DirectReports',
            'Displayname',
            'EmailAddress',
            'Enabled',
            'homeMDB',
            'ipPhone',
            'LastBadPasswordAttempt',
            'MemberOf',
            'MobilePhone',
            'Modified',
            'OfficePhone',
            'PasswordLastSet',
            'Title'
        )
       try
        {
            $P = Get-ADUser $Username -Properties $Properties | Select $Properties
            $Properties | foreach {
                $this.$_ = $P.$_
                }
        }
       Catch
        {
            $_; continue
        }
   }
}

I can create a custom object from this class in a couple of ways:


# create a new object
$me = New-Object -TypeName ADUser -ArgumentList tjones

# Instantiate using the 'new' static method
$me = [ADUser]::new('tjones')

# Simply set the variable type
[ADUSer]$me = 'tjones'

In each case, I need to pass a username, or actually any of the properties in this list, as this is required by the class constructor and the Get-ADUser cmdlet:
— A distinguished name
— A GUID (objectGUID)
— A security identifier (objectSid)
— A SAM account name (sAMAccountName)

When I create the object, the code runs and gets the info from AD.  When I call the variable I can see that the properties I have defined are populated:

captures

I can view the list of properties on the object using the dot operator…

capture

…or view an individual property…

capture

…or even find the value of a property without first storing it to a variable:

capture

Scope

This custom class is quite handy to quickly find out information about a user, and I want to be able to call this class any time I need it.  Trouble is, in the current implementation of this, a custom class is limited in scope and I can only use this class in the same context I’ve created it in.  Or to say it another way, I can’t change the class scope in the same way you could a variable for example.  You can of course change the scope of the object you create from the class, for example:


New-Variable -Name me -Value ([ADuser]::new('tjones')) -Scope Script

But once my current session or script is closed, this custom class is no longer available to me.  To use it again, I must add the class code and run it in each session or script so it is available in the current context.  This can make your scripts fatter than they need to be.

So here’s an idea:

Create a Class Library (of sorts)

In C# for example, you can create a library of classes as a dll file, but for PowerShell I can’t do that.

So why not simply create a folder of class code saved into text files, then read and run the code in your session?

Here’s one way to do that.  In my PowerShell $Profile directory, I have two folders “Modules” and “Scripts”.  I add a folder called “Classes”.

capture

I create the code for my custom class using PowerShell ISE, then save it as a text file to the Classes folder.  Any time I create a new custom class that I want to reuse later, I save it as a text file in this location.

capture

Now I can simply read the contents of a class file and run it in my current session with a one-liner to give me instant access to this custom class.


Invoke-Expression $([System.IO.File]::ReadAllText('C:\Users\tjones\Documents\WindowsPowerShell\Classes\ADUser.txt'))

I can also load in all the custom classes in the Classes directory into my session like this:

 

# Import Custom Classes
Get-ChildItem '$env:USERPROFILE\Documents\WindowsPowerShell\Classes\' |
    Select -ExpandProperty FullName |
        foreach {
            $class = [System.IO.File]::ReadAllText($_)
            Invoke-Expression $Class
        }

Even better, I can add this code into my PowerShell profile scripts (the Microsoft.Powershell_profile.ps1 and Microsoft.PowershellISE_profile.ps1) and all these classes will be available for me to use every time I start a new PowerShell session 🙂

Get Creative

This capability for custom classes in PowerShell opens a door of creativity for IT administrators and developers alike.  You can create simple classes to use in a script for example, or create reusable classes in a kind of class library as we have discussed, and you can add custom code that is relevant to the custom object so you can simply call a method instead of writing out the code each time.

How would custom classes be useful to you?  What kinds of objects and methods would be useful in your day to day work?

I do a lot of troubleshooting on remote computers, so I wrote a custom class that gets lots of useful information from a remote computer, or performs certain tasks on the remote computer.

In the example below, I’ve created a custom computer object representing “PC001”. Creating the object runs a quick “Test-Connection” against the machine, so I call the “Online” property to see if it is.  Then I get the current user using the “GetCurrentUser()” method, then I output a list of methods in the object.  These methods allow me to get information from Active Directory for the computer, get hardware information, get installed hotfixes or software, get local administrators and other useful things.

I can also start / stop services, or kill running processes.

capture

The custom type also contains a couple of static methods that I can use without creating an object and assigning it to a variable.  “GetComputerFromUser()” checks my Configuration Manager database to find the computer/s that a user was last logged into, and “GetSerialTag()” gets the serial number (or asset tag) for the remote computer.

capture

Cool stuff 🙂

Reading CCMEval Results Directly from a ConfigMgr Client with PowerShell

Since Configuration Manager 2012, a scheduled task is created by the client installer that runs “CCMEval.exe” periodically to check that the client is healthy and all the components it depends on are functioning as they should be. A number of checks are performed and if anything is found amiss, the default setting is to attempt remediation to fix the problem.  The client health status is reported to ConfigMgr and stored in the database.

By default, if a client has previously reported healthy and continues to be so, no new health report will be sent by the client to the management point, therefore the “last health evaluation time” reported in the console for active devices may be many months ago.  At first, this can seem disconcerting as it is tempting to think something may be wrong, but in reality the client is in all probability healthy.

If we check the ccmeval.log on the client, we can see that a report has recently been generated but it is “not necessary to send”.  The current report still exists on the client however as we will see later.

capture

It is possible to set the following registry key on the client which will cause the report to always be sent, that way ConfigMgr will always display the date of the most recent evaluation, but this will generate more traffic from the client and more work for ConfigMgr, especially if your client count is high, so probably isn’t best practice.

HKLM\SOFTWARE\Microsoft\CCM\CcmEval\SendAlways = TRUE

We can see the last health evaluation time from the ConfigMgr console, either in the Devices node under Assets and Compliance, or in the Client Check node under Monitoring.

capture

We can also find this information from the database directly with the following SQL query:


Select NetBiosName,ClientStateDescription,HealthCheckDescription,LastHealthEvaluation
from dbo.v_CH_ClientSummary ch
inner join dbo.v_CH_EvalResults eval on ch.ResourceID = eval.ResourceID
where NetBiosName = 'PC001'

 

capture

Yet if we look at the registry of the client itself (using PowerShell), we can see that it reports a much more recent date for the last evaluation time:


Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\CCM\CcmEval -Name LastEvalTime | Select -ExpandProperty LastEvalTime | Get-Date

capture

So in this case the difference between the last evaluation date in the database and the last evaluation date on the client itself is 12 months!  But that’s not a problem though, right? The client has been healthy for 12 months and hasn’t needed to report any further health data. Or has it?  What if the client is having trouble sending the current health evaluation report, therefore ConfigMgr is unaware of the actual current status?

That should be a rare occurrence since an inability to communicate with the management point will cause issues in other areas of client function and will probably result in the client being marked inactive in the console, but to be sure, lets look at the results of the most recent CCMEval task on the client.  When the CCMEval.exe runs, it creates an xml file called “CCMEvalReport.xml” with the results of the health evaluation at %windir%\CCM.  If you try to open this file however, you are greeted with an access denied warning:

capture

So first we need to take a copy of it, and open the copy.  Reading an XML file in notepad though is not too much fun:

capture

Of course, you can use an XML editor or a browser for better viewing, but since PowerShell can read XML nicely, lets create a PowerShell function that will open and parse this file into readable format for us.  We can then use this function to retrieve the CCMEval results from any local or remote computer.

First, since we need to run as administrator to take a copy of the file, let’s invoke a separate PowerShell instance using the “runas” verb so we don’t need to open an additional console window, and copy the file to a temp location:


$ComputerName -eq $env:COMPUTERNAME
$TargetFile = "$env:temp\CcmEvalReport.xml"
try
    {
        Start-Process -FilePath powershell.exe -ArgumentList "-Command ""&{Copy-Item $env:windir\CCM\CcmEvalReport.xml $TargetFile -Force}""" -Wait -ErrorAction Stop  -Verb Runas -WindowStyle Hidden
     }
catch
     {
          $_.Exception.Message
          continue
     }

Then let’s check that the file was copied and exists:


if (!(test-path $TargetFile))
    {
        Write-Error -Message "Could not locate the CcmEvalReport.xml"
        continue
    }

To load the file into PowerShell we need to create a new XML object and call the load method. Then we can remove the file from the temp location.


$xml = New-Object -TypeName System.Xml.XmlDocument
$xml.Load($TargetFile)
Remove-Item $TargetFile -Force

To run the previous code against either the local or a remote machine, we need to encapsulate the code in a script block, then call it using Invoke-Command. In addition, we will add a prompt for credentials in the case access is denied when using PS remoting:


$Script = {
    $TargetFile = ...
}

if ($ComputerName -eq $env:COMPUTERNAME)
{
    $xml = Invoke-Command -ScriptBlock $Script
}
Else
{
    try
    {
        $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -ErrorAction Stop
    }
    catch
    {
        if ($Error[0] | Select-String -Pattern 'Access is denied')
        {
            $Credentials = $host.ui.PromptForCredential('Credentials required', "Access was denied to $ComputerName.  Enter credentials to connect.", '', '')
            $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -Credential $Credentials
        }
        Else
        {
            $_.Exception.Message
        }
    }
} 

Now if we browse the $xml object, we can find 2 nodes of importance, a summary node:

capture

And a health check node:

capture

So let’s filter the information we need, and add the results to objects that we can output with our function:


$Checks = $xml.ClientHealthReport.HealthChecks.HealthCheck |
Select-Object -Property @{
    l = 'Test'
    e = {
        $_.Description
    }
}, @{
    l = 'Result'
    e = {
        $_.'#text'
    }
} |
Sort-Object -Property Test

$Summary = $xml.ClientHealthReport.Summary |
Select-Object -Property @{
    l = 'ComputerName'
    e = {
        $ComputerName
    }
}, @{
    l = 'EvaluationTime'
    e = {
        [datetime]($_.EvaluationTime)
    }
}, @{
    l = 'Result'
    e = {
        $_.'#text'
    }
}

Finally, we add this code into a function so we can easily run it against the local computer, or pass a remote computername/s to it as a parameter or along the pipeline to read the CCMEval results from other computers.

Get-CCMEvalResult


function Get-CCMEvalResult
{
    <#             .Synopsis             Get the results of the most recent client health evaluation on a local or remote computer             .DESCRIPTION             Parses the ccmevalreport.xml file into a readable format to view the results of the ccmeval task.  Can be run on the local or remote computer.             .EXAMPLE             Get-CCMEvalResult             Returns the ccmeval results from the local machine             .EXAMPLE             Get-CCMEvalResult -ComputerName PC001             Returns the ccmeval results from a remote machine             .EXAMPLE             'PC001','PC002' | Get-CCMEvalResult             Returns the ccmeval results from a remote machine     #>

    #requires -Version 2

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false,
                ValueFromPipeline = $true
        )]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    Begin {
        $Script = {
            $TargetFile = "$env:temp\CcmEvalReport.xml"
            try
            {
                Start-Process -FilePath powershell.exe -ArgumentList "-Command ""&{Copy-Item $env:windir\CCM\CcmEvalReport.xml $TargetFile -Force}""" -Wait -ErrorAction Stop  -Verb Runas -WindowStyle Hidden
            }
            catch
            {
                $_.Exception.Message
                continue
            }

            if (!(test-path $TargetFile))
                {
                    Write-Error -Message "Could not locate the CcmEvalReport.xml"
                    continue
                }

            $xml = New-Object -TypeName System.Xml.XmlDocument
            $xml.Load($TargetFile)
            Remove-Item $TargetFile -Force
            return $xml
        }
    }

    Process {
        if ($ComputerName -eq $env:COMPUTERNAME)
        {
            $xml = Invoke-Command -ScriptBlock $Script
        }
        Else
        {
            try
                {
                    $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -ErrorAction Stop
                }
            catch
                {
                    if ($Error[0] | Select-String -Pattern 'Access is denied')
                        {
                            $Credentials = $host.ui.PromptForCredential('Credentials required', "Access was denied to $Computername.  Enter credentials to connect.", '', '')
                            $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -Credential $Credentials
                        }
                    Else { $_.Exception.Message }
                }
        }

        $Checks = $xml.ClientHealthReport.HealthChecks.HealthCheck |
        Select-Object -Property @{
            l = 'Test'
            e = {
                $_.Description
            }
        }, @{
            l = 'Result'
            e = {
                $_.'#text'
            }
        } |
        Sort-Object -Property Test
        [array]$Summary = $xml.ClientHealthReport.Summary |
        Select-Object -Property @{
            l = 'ComputerName'
            e = {
                $ComputerName
            }
        }, @{
            l = 'EvaluationTime'
            e = {
                [datetime]($_.EvaluationTime)
            }
        }, @{
            l = 'Result'
            e = {
                $_.'#text'
            }
        }

        $Summary
        $Checks | Format-Table
    }
}

Now if I run the function against my local machine, I can see each health test performed and the results, as well as the overall status of the evaluation and the time it was performed.

capture

I can also run it against remote computers like this:


Get-CCMEvalResult -ComputerName 'SRV001'
'PC001','PC001' | Get-CCMEvalResult

To troubleshoot any evaluation failures, view the ccmeval.log (%windir%\CCM) and the ccmsetup-ccmeval.log (%windir%\ccmsetup\Logs).

New tool! Reliability Viewer for Windows

bs

I am pleased to release a new free GUI tool today to assist in troubleshooting Windows systems – the Reliability Viewer for Windows.  It is based on the concept of the built-in Windows Reliability Monitor, but can be used to display data from both the local or remote systems, which the Microsoft tool cannot do.

It is useful for identifying stability issues on a Windows system by reporting key system events such as

  • Application crashes
  • Software Update installations
  • MsiInstaller events
  • Unexpected system shutdowns
  • Blue-screens
  • Driver installations
  • Hardware failure

It uses PowerShell Remoting to retrieve reliability data from remote computers and displays it in an event-log style grid format.  Filters are included for the key fields for easy searching of particular events. The application can also generate a system stability chart using Microsoft Excel, which uses reliability metrics to give an overview of the stability of the system over time.

RelChart

Download the tool here, or read the full blog for the tool here.

Enjoy 🙂