Object Magic (Part 1)

In PowerShell, most data are represented as PSObjects, a specific object “wrapper” added by PowerShell. To get to this specific wrapper, objects have a secret property called “PSObject”. Let’s take a look:

# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject

# get another object
$object = "Hello"

# try again
$object.PSObject

As you’ll see, the secret “PSObject” is basically a description for the object. And there is a ton of useful information embedded. Here’s some:

# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject

# find useful information
$object.PSObject.TypeNames | Out-GridView -Title Type
$object.PSObject.Properties | Out-GridView -Title Properties
$object.PSObject.Methods | Out-GridView -Title Methods

Twitter This Tip! ReTweet this Tip!

Encrypting Text (Part 2)

This is the second part of our text encryption/decryption series. In the first part you learned how you can safely encrypt text on a machine. Now let’s take a look at the decrypting part.

To successfully decrypt text, you must specify the same encoding that was used during encryption. Based on your encryption parameters, you must specify the same password, and based on the -Scope settings, decryption will work only for you and/or only on the same machine where you encrypted the text.

Here is the function Unprotect-Text. We also copied the Protect-Text function from our last tip so you have both handy:

function Protect-Text
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [String]
        $SecretText,
        
        [string]
        $Password='',
        
        [string]
        [ValidateSet('CurrentUser','LocalMachine')]
        $scope = 'CurrentUser',

        [string]
        [ValidateSet('UTF7','UTF8','UTF32','Unicode','ASCII','Default')]
        $Encoding = 'Default',

        [Switch]
        $ReturnByteArray
        
    )
    begin
    {
        Add-Type -AssemblyName System.Security
        if ([string]::IsNullOrEmpty($Password))
        {
            $optionalEntropy = $null
        }
        else
        {
            $optionalEntropy = [System.Text.Encoding]::$Encoding.GetBytes($Password)
        }
    }
    process
    {
        try
        {
            $userData = [System.Text.Encoding]::$Encoding.GetBytes($SecretText)
            $bytes = [System.Security.Cryptography.ProtectedData]::Protect($userData, $optionalEntropy, $scope)
            if ($ReturnByteArray)
            {
                $bytes
            }
            else
            {
                [Convert]::ToBase64String($bytes)
            }
        }
        catch
        {
            throw "Protect-Text: Unable to protect text. $_"
        }
    }
}



function Unprotect-Text
{
    [CmdletBinding(DefaultParameterSetName='Byte')]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ParameterSetName="Text", Position=0)]
        [string]
        $EncryptedString,

        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ParameterSetName="Byte", Position=0)]
        [Byte[]]
        $EncryptedBytes,
        
        [string]
        $Password='',
        
        [string]
        [ValidateSet('CurrentUser','LocalMachine')]
        $scope = 'CurrentUser',

        [string]
        [ValidateSet('UTF7','UTF8','UTF32','Unicode','ASCII','Default')]
        $Encoding = 'Default'
        
    )
     begin
    {
        Add-Type -AssemblyName System.Security

        if ([string]::IsNullOrEmpty($Password))
        {
            $optionalEntropy = $null
        }
        else
        {
            $optionalEntropy = [System.Text.Encoding]::$Encoding.GetBytes($Password)
        }
    }
    process
    {
        try
        {
            if ($PSCmdlet.ParameterSetName -eq 'Text')
            {
                $inBytes = [Convert]::FromBase64String($EncryptedString)
            }
            else
            {
                $inBytes = $EncryptedBytes
            }
            $bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($inBytes, $optionalEntropy, $scope)
            [System.Text.Encoding]::$Encoding.GetString($bytes)
        }
        catch
        {
            throw "Unprotect-Text: Unable to unprotect your text. Check optional password, and make sure you are using the same encoding that was used during protection."
        }
    }
}

And here is an example on how to use it:

$text = "This is my secret"

$a = Protect-Text -SecretText $text -scope CurrentUser -Password zumsel 

Unprotect-Text -EncryptedString $a -scope CurrentUser -Password zumsel 

Protect-Text creates a Base64-encoded string that can be decrypted using Unprotect-Text. Omit the -Password parameter if you don’t want to set an additional password. If you don’t specify a password, you get the default protection only, based on -Scope.

To save space, you can use a byte array instead of a Base64-encoded string:

$b = Protect-Text -SecretText $text -scope CurrentUser -ReturnByteArray
Unprotect-Text -EncryptedBytes $b -scope CurrentUser

Twitter This Tip! ReTweet this Tip!

Encrypting Text (Part 1)

Let’s take a look at a safe way of encrypting text on a computer. The below Protect-Text function takes any text and encrypts it automatically, no password needed. Instead of a password, it uses either your user account and machine, or just your machine as a secret.

If you use -Scope LocalMachine, any user on that machine can decrypt the text, but if the text is leaked to someone else, it cannot be decrypted on another machine. If you use -Scope CurrentUser, only the person that encrypted the text can decrypt it, and only on the machine where it was encrypted. This encryption method is ideal for saving your own private passwords.

In addition, and to increase security, an (optional) password can be added on top. When specified, the same restrictions apply, but in addition the password needs to be known to decrypt the text.

Note that you can also control the text encoding. Make sure to use the same encoding for both encryption and decryption.

Here is the encryption function:

function Protect-Text
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [String]
        $SecretText,
        
        [string]
        $Password='',
        
        [string]
        [ValidateSet('CurrentUser','LocalMachine')]
        $scope = 'CurrentUser',

        [string]
        [ValidateSet('UTF7','UTF8','UTF32','Unicode','ASCII','Default')]
        $Encoding = 'Default',

        [Switch]
        $ReturnByteArray
        
    )
    begin
    {
        Add-Type -AssemblyName System.Security
        if ([string]::IsNullOrEmpty($Password))
        {
            $optionalEntropy = $null
        }
        else
        {
            $optionalEntropy = [System.Text.Encoding]::$Encoding.GetBytes($Password)
        }
    }
    process
    {
        try
        {
            $userData = [System.Text.Encoding]::$Encoding.GetBytes($SecretText)
            $bytes = [System.Security.Cryptography.ProtectedData]::Protect($userData, $optionalEntropy, $scope)
            if ($ReturnByteArray)
            {
                $bytes
            }
            else
            {
                [Convert]::ToBase64String($bytes)
            }
        }
        catch
        {
            throw "Protect-Text: Unable to protect text. $_"
        }
    }

And this would be a result:

 
PS> Protect-Text -SecretText 'I am encrypted' -scope LocalMachine 
AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAF8Hpm9A5A0upZysoxLlvgwQAAAACAAAAAAAQZgAAAAEAACAAAACbOsUoDuZJXNkWIzfAABxktVg+Txn7A8Rz
SCvFP7I9YQAAAAAOgAAAAAIAACAAAABz7G7Tpuoje9meLOuugzx1WSoOUfaBtGPM/XZHytjC8hAAAAApt/TDhJ9EqeWEPLIDkd4bQAAAAAN0Q503Pa7X
MxIMOnaO7qd3LKXJa4qhht+jc+Z0HaaV5/md83ipP1vefYAAUXdj8qv4eREeCBSGMqvKjbaOsOg=  
 

If you’re wondering how to turn this Base64-encoded text back to the original text, check our next tip!


Twitter This Tip! ReTweet this Tip!

Creating NT4 Password Hashes

Internally, Active Directory stores all passwords as so-called NTLM Hashes. There are a number of security-analysis tools that can read and dump these hashes.

While there fortunately is no feasible way of decrypting these hashes and retrieving the original passwords, you can take a (known) password and turn it into an NTLM hash yourself. This is the fundamental procedure of dictionary attacks: they take long lists of “known passwords”, turn them into NTLM hashes, and when they match an actual AD account hash, the password is known.

This way, your security department could take password blacklist with insecure passwords such as “[email protected]”, turn them into NTLM hashes, and compare them to the password hashes of your Active Directory to identify accounts that need a password change.

Here is PowerShell code to turn a plain text into an NTLM hash:

function ConvertTo-NTLMPasswordHash
{
  #Work based on code found here: https://www.myotherpcisacloud.com/post/getmd4hash
  #Original Author: Ryan Ries, 2014 
  param(
    [Parameter(Mandatory=$true)][string]$password
  )

  Function Get-MD4Hash
  {
    Param ([Parameter(Mandatory=$True, ValueFromPipeline=$False)]          
    [Byte[]]$DataToHash)
           
      Set-StrictMode -Version Latest
      Add-Type -TypeDefinition @'
        using System;
        using System.Text;
        using System.Runtime.InteropServices;
        public class BCrypt
        {
            [DllImport("bcrypt.dll", CharSet = CharSet.Auto)]
            public static extern NTStatus BCryptOpenAlgorithmProvider(
                [Out] out IntPtr phAlgorithm,
                [In] string pszAlgId,
                [In, Optional] string pszImplementation,
                [In] UInt32 dwFlags);
 
            [DllImport("bcrypt.dll")]
            public static extern NTStatus BCryptCloseAlgorithmProvider(
                [In, Out] IntPtr hAlgorithm,
                [In] UInt32 dwFlags);
 
            [DllImport("bcrypt.dll", CharSet = CharSet.Auto)]
            public static extern NTStatus BCryptCreateHash(
                [In, Out] IntPtr hAlgorithm,
                [Out] out IntPtr phHash,
                [Out] IntPtr pbHashObject,
                [In, Optional] UInt32 cbHashObject,
                [In, Optional] IntPtr pbSecret,
                [In] UInt32 cbSecret,
                [In] UInt32 dwFlags);
 
            [DllImport("bcrypt.dll")]
            public static extern NTStatus BCryptDestroyHash(
                [In, Out] IntPtr hHash);
 
            [DllImport("bcrypt.dll")]
            public static extern NTStatus BCryptHashData(
                [In, Out] IntPtr hHash,
                [In, MarshalAs(UnmanagedType.LPArray)] byte[] pbInput,
                [In] int cbInput,
                [In] UInt32 dwFlags);
 
            [DllImport("bcrypt.dll")]
            public static extern NTStatus BCryptFinishHash(
                [In, Out] IntPtr hHash,
                [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbInput,
                [In] int cbInput,
                [In] UInt32 dwFlags);
 
            [Flags]
            public enum AlgOpsFlags : uint
            {           
                BCRYPT_PROV_DISPATCH = 0x00000001,
                BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008,
                BCRYPT_HASH_REUSABLE_FLAG = 0x00000020
            }
 
            // This is a gigantic enum and I don't want to copy all of it into this Powershell script.
            // Basically anything other than zero means something went wrong.
            public enum NTStatus : uint
            {
                STATUS_SUCCESS = 0x00000000
            }
        }
'@
 
      [Byte[]]$HashBytes   = New-Object Byte[] 16
      [IntPtr]$PHAlgorithm = [IntPtr]::Zero
      [IntPtr]$PHHash      = [IntPtr]::Zero
      $NTStatus = [BCrypt]::BCryptOpenAlgorithmProvider([Ref] $PHAlgorithm, 'MD4', $Null, 0)
      If ($NTStatus -NE 0)
      {
        Write-Error "BCryptOpenAlgorithmProvider failed with NTSTATUS $NTStatus"
        If ($PHAlgorithm -NE [IntPtr]::Zero)
        {
          $NTStatus = [BCrypt]::BCryptCloseAlgorithmProvider($PHAlgorithm, 0)
        }
        Return
      }
      $NTStatus = [BCrypt]::BCryptCreateHash($PHAlgorithm, [Ref] $PHHash, [IntPtr]::Zero, 0, [IntPtr]::Zero, 0, 0)
      If ($NTStatus -ne 0)
      {
        Write-Error "BCryptCreateHash failed with NTSTATUS $NTStatus"
        If ($PHHash -ne [IntPtr]::Zero)
        {
          $NTStatus = [BCrypt]::BCryptDestroyHash($PHHash)               
        }
        If ($PHAlgorithm -ne [IntPtr]::Zero)
        {
          $NTStatus = [BCrypt]::BCryptCloseAlgorithmProvider($PHAlgorithm, 0)
        }
        Return
      }
 
      $NTStatus = [BCrypt]::BCryptHashData($PHHash, $DataToHash, $DataToHash.Length, 0)
      $NTStatus = [BCrypt]::BCryptFinishHash($PHHash, $HashBytes, $HashBytes.Length, 0)
 
      If ($PHHash -NE [IntPtr]::Zero)
      {
        $NTStatus = [BCrypt]::BCryptDestroyHash($PHHash)
      }
      If ($PHAlgorithm -NE [IntPtr]::Zero)
      {
        $NTStatus = [BCrypt]::BCryptCloseAlgorithmProvider($PHAlgorithm, 0)
      }
         
      $HashString = New-Object System.Text.StringBuilder
      Foreach ($Byte In $HashBytes)
      {
        $null = $HashString.Append($Byte.ToString("x2"))
      }
      $HashString.ToString()
    
  }
  Get-MD4Hash -DataToHash ([System.Text.Encoding]::Unicode.getBytes($password))
}

Twitter This Tip! ReTweet this Tip!

Simple PowerShell Chat

Here’s a fun PowerShell script that you can use to create a simple multi-channel chat room. All you need is a network share where everyone has read and write permissions.

The chat is file-based and makes use of PowerShell’s ability to monitor files for changes. So essentially, each chat channel is a text file, and whenever someone wants to “say” something, a text line is added to the file. Anyone connected to the chat actually monitors this file for changes.

Obviously, this “chat” is just for experimenting and has a number of limitations. For example, when someone writes to the file, no one else can write to it. However, it nicely illustrates how PowerShell can monitor files and take action when new text is added. You can take this technique and apply it to log files as well: PowerShell can notify you when log files are appended, and even auto-filter the newly added text to send out warnings or take action on trigger words.

Before you start, make sure you adjust $ServerShare and set it to a read/write network share.

Next, you can enter the chat like so:

 
Enter-Chat -ChatChannelName lunchbreak -Name Tobias -ShowOldPosts 
 

-ShowOldPosts echos the existing chat messages. Without this, you see only new messages. Whenever you run Enter-Chat, it checks for a file with the name specified in -ChatChannelName, and if it is still missing, the file is created.

Get-ChatChannel lists all chat files found on the share, plus the time the channel was last used. This information is taken entirely from the file properties (LastWriteTime).

# make sure you adjust this path
# it must point to a network share where you have read and write permissions
$ServerShare = "\myserverchathome"

function Enter-Chat 
{
  param
  (
    [Parameter(Mandatory)]
    [string]
    $ChatChannelName,
    
    [string]
    $Name = $env:USERNAME,
    
    [Switch]
    $ShowOldPosts,
    
    $HomeShare = $ServerShare
    
  )
  
  if ($ShowOldPosts)
  {
    $Option = ''
  }
  else
  {
    $Option = '-Tail 0'
  }

  $Path = Join-Path -Path $HomeShare -ChildPath "$ChatChannelName.txt"
  $exists = Test-Path -Path $Path
  if ($exists -eq $false)
  {
    $null = New-Item -Path $Path -Force -ItemType File
  }

  $process = Start-Process -FilePath powershell -ArgumentList "-noprofile -windowstyle hidden -command Get-COntent -Path '$Path' $Option -Wait | Out-GridView -Title 'Chat: [$ChatChannelName]'" -PassThru

  Write-Host "To exit, enter: quit"
  "[$Name entered the chat]" | Add-Content -Path $Path
  do
  {
    Write-Host "[$ChatChannelName]: " -ForegroundColor Green -NoNewline
    $inputText = Read-Host 
    
    $isStopCommand = 'quit','exit','stop','leave' -contains $inputText
    if ($isStopCommand -eq $false)
    {
      "[$Name] $inputText" | Add-Content -Path $Path
    }
    
    
  } until ($isStopCommand -eq $true)
  "[$Name left the chat]" | Add-Content -Path $Path
  
  $process | Stop-Process
}



function Get-ChatChannel
{
  param
  (
    $HomeShare = $ServerShare
    
  )

  Get-ChildItem -Path $HomeShare -Filter *.txt -File |
    ForEach-Object {
      [PSCustomObject]@{
        ChannelName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
        LastActive = $_.LastWriteTime
        Started = $_.CreationTime
      }
    }
}

Twitter This Tip! ReTweet this Tip!

Testing Password Strength

In previous tips, we already talked about services such as haveIbeenpwned.com. They harvest leaked passwords from previous hacker attacks so you can check whether your password has been compromised and is likely to be included in future dictionary attacks.

Below, you find two useful functions: Test-Password asks for a SecureString so when you get prompted, your input is masked. Convert-SecureStringToText then converts the entered password to plain text. Test-Password then hashes your entered password and sends only the first 5 bytes to the web service. Your original password is never compromised.

The web service returns all compromised password hashes that start with your 5 bytes, so you can check whether one of the returned hashes matches your hash.

If your password has been seen in previous attacks, you’ll receive the number of times it was involved in attacks. Any password that returns a number greater than 0 must be considered insecure, and should not be used.

Please remember: when a password returns a number greater than 0, this does not mean that your own password was compromised. It does mean that anywhere in the world, your password surfaced during an attack, so either you or anyone else has used it and was hacked. No matter whether it was you or someone else: this password is now insecure because it became part of attack dictionaries that hacker will try on accounts. If you continue to use this password, chances are high that hackers will break into it using simple (and fast) dictionary attacks anytime soon in the future.

function Convert-SecureStringToText
{
  param
  (
    [Parameter(Mandatory,ValueFromPipeline)]
    [System.Security.SecureString]
    $Password
  )
  
  process
  {
    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
    [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
  }
}

function Test-Password
{
  [CmdletBinding()]
  param
  (
    [Parameter(Mandatory, Position=0)]
    [System.Security.SecureString]
    $Password
  )
  
  $plain = $Password | Convert-SecureStringToText
  
  $bytes = [Text.Encoding]::UTF8.GetBytes($plain)
  $stream = [IO.MemoryStream]::new($bytes)
  $hash = Get-FileHash -Algorithm 'SHA1' -InputStream $stream
  $stream.Close()
  $stream.Dispose()
  
  $first5hashChars,$remainingHashChars = $hash.Hash -split '(?<=^.{5})'
  
  $url = "https://api.pwnedpasswords.com/range/$first5hashChars"
  [Net.ServicePointManager]::SecurityProtocol = 'Tls12'
  $response = Invoke-RestMethod -Uri $url -UseBasicParsing
  
  $lines = $response -split 'rn'
  $filteredLines = $lines -like "$remainingHashChars*"
  
  [int]($filteredLines -split ':')[-1]
}


Twitter This Tip! ReTweet this Tip!