All posts by Freakling

Sending css formatted tables in Outlook

If you’ve ever used Powershell to send HTML tables in Outlook containing CSS you’ve probably been disappointed of the outcome.
There is some archived documentation for Outlook 2007 that is still viable for Outlook 365 (https://msdn.microsoft.com/en-us/library/aa338201(v=office.12).aspx).

Basically the function accepts a csv and css file, hardcodes the css into the table and outputs a formatted HTML table that is compatible with Outlook.

Read more on: https://tech.xenit.se/sending-css-formatted-tables-outlook/

Just enough Administration & RDS

Reblogged from my company blog: https://tech.xenit.se/just-enough-administration-rds/

The Problem?

Microsoft RDS has limitations when delegating access, in fact there is no built-in access delegation whatsoever!

The solution? Powershell!

A Just enough Administration (JEA) endpoint, also known as a Constrained Powershell Endpoint.
The endpoint is restricted to only access List collection, list users and subsequently force logoff users. The endpoint configuration to only accept users from a certain AD group.

To configure endpoint, run the following code on all connection brokers. The script is somewhat generalized for easy adaptability. The main parts are in the parameters where we configure who can run, using which account and also what cmdlet are available in the constrained session.

<#
.Synopsis
    Short description
.DESCRIPTION
    
.NOTES
    Name: Create-JEA-RDS-Endpoint.ps1
    Author: Vikingur Saemundsson
    Date Created: 2017-11-20
    Version History:
        2017-11-20 - Vikingur Saemundsson
            Initial Creation

    Xenit AB
#>

[cmdletbinding()]
Param(
    [String]$Name = 'Xenit.JEA.RDS.Servicedesk',
    [String]$Path = 'C:\Xenit\RDS\LogoffEndpoint',
    [Array]$VisibleCmdlets = $("Get-RDSessionCollection","Get-RDSessionHost","Get-RDUserSession","Invoke-RDUserLogoff","Where-object"),
    [String]$AdGroup = 'Servicedesk Ad group',
    [Parameter(Mandatory=$true)]
    [String]$ServiceAccount,
    [Array]$Modules = 'RemoteDesktop',
    [String]$Author = 'Vikingur Saemundsson',
    [String]$Company = 'Xenit AB'
)
Try{
    $ADGroupSID = (Get-ADGroup $AdGroup).SID

    If(-not(Test-Path -Path $Path -PathType Container)){
        New-Item -Path $Path -ItemType Directory -Force
    }

    $LockdownScript = @'
    Get-Command | Where Visibility -eq 'Public' | ForEach-Object {
        if ( $_.Name -notin $CmdsToExclude ) {
            $_.Visibility = 'Private'
        }
    }
'@ 
    '$CmdsToExclude = @("Get-Command", "Out-Default", "Exit-PSSession", "Measure-Object", "Select-Object" , "Get-FormatData"{0})' -f (($VisibleCmdlets | ForEach-Object{",`"$_`""}) -join '') | Out-File "$Path\LockdownScript.ps1"
    $LockdownScript | Out-File "$Path\LockdownScript.ps1" -Append

    $Modules += 'Microsoft.PowerShell.Core'
    $Modules += 'Microsoft.PowerShell.Management'
    $Modules += 'Microsoft.PowerShell.Security'
    $Modules += 'Microsoft.PowerShell.Utility'
    $Modules += 'Microsoft.PowerShell.Diagnostics'
    $Modules += 'Microsoft.PowerShell.Host'

    $ConfigFileParams = @{
        Path ="$Path\$Name.pssc"
        ModulesToImport = $Modules
        ScriptsToProcess = "$Path\LockdownScript.ps1"
        CompanyName = $Company
        Author = $Author
        Copyright = "(c) $((Get-Date).Year) $Company. All rights reserved."
        SessionType = 'Default'
        LanguageMode = "ConstrainedLanguage"
        ExecutionPolicy = 'Bypass'
    }

    $SessionConfigParams = @{
        RunAsCredential = $ServiceAccount
        Name = $Name
        Path = $ConfigFileParams.Path
        UseSharedProcess = $true
        SecurityDescriptorSddl = "O:BAG:DUD:AI(A;;GX;;;$ADGroupSID)"
        Confirm = $false
    }

    If(Get-PSSessionConfiguration -Name $SessionConfigParams.Name -ErrorAction SilentlyContinue){
        Unregister-PSSessionConfiguration -Name $SessionConfigParams.Name -Confirm:$false
    }

    New-PSSessionConfigurationFile @ConfigFileParams

    Register-PSSessionConfiguration @SessionConfigParams
}
Catch{
    Write-Error $_
}

The frontend application code is not posted in the blog.

Recursively search Azure Ad group members

Reblogged from my company blog: https://tech.xenit.se/recursively-search-azure-ad-group-members/

When working with on-premise Active Directory an administrator often has to recursively search AD groups, this is easy using the ActiveDirectory module with cmdlet “Get-AdGroupMember <Group> -Recusive”.
For the AzureAD equivalent this is no longer an option, the cmdlet Get-AzureADGroupMember has three parameters.

PARAMETERS
-All <Boolean>
If true, return all group members. If false, return the number of objects specified by the Top parameter
-ObjectId <String>
Specifies the ID of a group in Azure AD.
-Top <Int32>
Specifies the maximum number of records to return.

As we can see there is no -recursive, in order to search recursively I’ve written the function below.

Function Get-RecursiveAzureAdGroupMemberUsers{
[cmdletbinding()]
param(
   [parameter(Mandatory=$True,ValueFromPipeline=$true)]
   $AzureGroup
)
    Begin{
        If(-not(Get-AzureADCurrentSessionInfo)){Connect-AzureAD}
    }
    Process {
        Write-Verbose -Message "Enumerating $($AzureGroup.DisplayName)"
        $Members = Get-AzureADGroupMember -ObjectId $AzureGroup.ObjectId -All $true
        
        $UserMembers = $Members | Where-Object{$_.ObjectType -eq 'User'}
        If($Members | Where-Object{$_.ObjectType -eq 'Group'}){
            $UserMembers += $Members | Where-Object{$_.ObjectType -eq 'Group'} | ForEach-Object{ Get-RecursiveAzureAdGroupMemberUsers -AzureGroup $_}
        }
    }
    end {
        Return $UserMembers
    }
}

The function accepts groups by parameter or by pipeline and returns only the object type ‘User’

To run a recursive AzureAD Group member search simply pipe a normal ADgroup search as below

Get-AzureADGroup -SearchString 'AzureADGroupName' | Get-RecursiveAzureAdGroupMemberUsers

Azure Automation – Running scripts locally on VM through runbooks

Reblogged from my company blog: https://tech.xenit.se/azure-automation-running-scripts-locally-vm-runbooks/

I was tasked to create a powershell script to run on a schedule on a Azure VM. Normally this would be running as a scheduled task on the VM but seeing as we’re working with AzureVM and schedule tasks are legacy I wanted to explore the possibilities of running the schedule and script in Azure to keep the VM clean and the configuration scalable.

After some research the best option would be running the powershell script as a CustomScriptExtension on the VM, and the schedule would be handled by a Process Automation Runbook (using Automation Accounts).

What I ended up with is the script below. It’s fairly easy to configure and contains almost all the required configuration in the parameters.

How does it work? Simple!

Prerequisites

  1. Create a Storage Account
    • Create a Private Blob container
  2. Create a Automation Account
    • Make sure a RunAsAccount is created

Runbook configuration

  1. Navigate to the Automation Account you intend to use
  2. Create a Powershell runbook and press Edit
  3. Copy the below script into the runbook and save
    • Fill out the parameters with relevant information (subscription, resourcegroup etc)
    • Make sure to set the name of the StorageContainer to the one where you want to host the scripts
    • Extension name should be unique for the job, as well as the ScriptName
    • The scriptblock parameter takes the script you intend to run on the VMs
  4. On the first run, go into the Test Pane from edit view and edit the UploadScript parameter to $True
    • This will make the runbook actually save the script to the Container, allowing the VM download and run the script
  5. Done!

Now simply register schedules to the runbook. If you want to run the script on several VM you have two options. Either specify multiple VM when creating schedules or create a schedule per machine you want to run the script against.

[cmdletbinding()]
Param(
    $SubscriptionID = '<SubscriptionID>',
    $ResourceGroup = 'RG-NorthEU',
    $VMNames = @('vmname1','vmname2'),
    $StorageAccount = '<StorageAccount>',
    $StorageAccountKey = '<Storagekey>',
    $StorageContainer = 'sc-scripts', # Create this manually before first execution!
    $ExtensionName = 'Xenit.<job>',
    $ScriptName = 'scriptname.ps1',
    
    $UploadScript = $false,
    $ScriptBlock = {
        <Script to run locally on VMs>
    }
)


################
################
#
# DO NOT CHANGE BELOW
#
################
################

function Invoke-AzureRmVmScript {
<#
    
    # Vikingur Saemundsson @ Xenit AB
    # Credit to source https://github.com/RamblingCookieMonster/PowerShell/blob/master/Invoke-AzureRmVmScript.ps1
    # Made som small modifications to allow control over if the scriptfile is uploaded or not to avoid extra data to containers and shorten the executiontime
    #

    .FUNCTIONALITY
        Azure
#>
    [cmdletbinding()]
    param(
        # todo: add various parameter niceties
        [Parameter(Mandatory = $True,
                    Position = 0,
                    ValueFromPipelineByPropertyName = $True)]
        [string[]]$ResourceGroupName,
        
        [Parameter(Mandatory = $True,
                    Position = 1,
                    ValueFromPipelineByPropertyName = $True)]
        [string[]]$VMName,
        
        [Parameter(Mandatory = $True,
                    Position = 2)]
        [scriptblock]$ScriptBlock, #todo: add file support.
        
        [Parameter(Mandatory = $True,
                    Position = 3)]
        [string]$StorageAccountName,

        [string]$StorageAccountKey, #Maybe don't use string...

        $StorageContext,
        
        [string]$StorageContainer = 'sc-scripts',
        
        [string]$Filename, # Auto defined if not specified...
        
        [string]$ExtensionName, # Auto defined if not specified

        [bool]$UploadScript = $true,

        [switch]$ForceExtension,
        [switch]$ForceBlob,
        [switch]$Force
    )
    begin
    {
        if($Force)
        {
            $ForceExtension = $True
            $ForceBlob = $True
        }
    }
    process
    {
        Foreach($ResourceGroup in $ResourceGroupName)
        {
            Foreach($VM in $VMName)
            {
                if(-not $Filename)
                {
                    $GUID = [GUID]::NewGuid().Guid -replace "-", "_"
                    $FileName = "$GUID.ps1"
                }
                if(-not $ExtensionName)
                {
                    $ExtensionName = $Filename -replace '.ps1', ''
                }

                $CommonParams = @{
                    ResourceGroupName = $ResourceGroup
                    VMName = $VM
                }

                Write-Verbose "Working with ResourceGroup $ResourceGroup, VM $VM"
                # Why would Get-AzureRMVmCustomScriptExtension support listing extensions regardless of name? /grumble
                Try
                {
                    $AzureRmVM = Get-AzureRmVM @CommonParams -ErrorAction Stop
                    $AzureRmVMExtended = Get-AzureRmVM @CommonParams -Status -ErrorAction Stop
                }
                Catch
                {
                    Write-Error $_
                    Write-Error "Failed to retrieve existing extension data for $VM"
                    continue
                }

                # Handle existing extensions
                Write-Verbose "Checking for existing extensions on VM '$VM' in resource group '$ResourceGroup'"
                $Extensions = $null
                $Extensions = @( $AzureRmVMExtended.Extensions | Where {$_.Type -like 'Microsoft.Compute.CustomScriptExtension'} )
                if($Extensions.count -gt 0)
                {
                    Write-Verbose "Found extensions on $VM`:`n$($Extensions | Format-List | Out-String)"
                    if(-not $ForceExtension)
                    {
                        Write-Warning "Found CustomScriptExtension '$($Extensions.Name)' on VM '$VM' in Resource Group '$ResourceGroup'.`n Use -ForceExtension or -Force to remove this"
                        continue
                    }
                    Try
                    {
                        # Theoretically can only be one, so... no looping, just remove.
                        $Output = Remove-AzureRmVMCustomScriptExtension @CommonParams -Name $Extensions.Name -Force -ErrorAction Stop
                        if($Output.StatusCode -notlike 'OK')
                        {
                            Throw "Remove-AzureRmVMCustomScriptExtension output seems off:`n$($Output | Format-List | Out-String)"
                        }
                    }
                    Catch
                    {
                        Write-Error $_
                        Write-Error "Failed to remove existing extension $($Extensions.Name) for VM '$VM' in ResourceGroup '$ResourceGroup'"
                        continue
                    }
                }
                
                if(-not $StorageContainer)
                {
                    $StorageContainer = 'scripts'
                }
                if(-not $Filename)
                {
                    $Filename = 'CustomScriptExtension.ps1'
                }
                if(-not $StorageContext)
                {
                    if(-not $StorageAccountKey)
                    {
                        Try
                        {
                            $StorageAccountKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $ResourceGroup -Name $storageAccountName -ErrorAction Stop)[0].value
                        }
                        Catch
                        {
                            Write-Error $_
                            Write-Error "Failed to obtain Storage Account Key for storage account '$StorageAccountName' in Resource Group '$ResourceGroup' for VM '$VM'"
                            continue
                        }
                    }
                    Try
                    {
                        $StorageContext = New-AzureStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey
                    }
                    Catch
                    {
                        Write-Error $_
                        Write-Error "Failed to generate storage context for storage account '$StorageAccountName' in Resource Group '$ResourceGroup' for VM '$VM'"
                        continue
                    }
                }
                If($UploadScript){
                    Write-Verbose "Uploading script to storage account $StorageAccountName"
                    Try
                    {
                        $Script = $ScriptBlock.ToString()
                        $LocalFile = [System.IO.Path]::GetTempFileName()
                        Start-Sleep -Milliseconds 500 #This might not be needed
                        Set-Content $LocalFile -Value $Script -ErrorAction Stop
            
                        $params = @{
                            Container = $StorageContainer
                            Context = $StorageContext
                        }

                        $Existing = $Null
                        $Existing = @( Get-AzureStorageBlob @params -ErrorAction Stop )

                        if($Existing.Name -contains $Filename -and -not $ForceBlob)
                        {
                            Write-Warning "Found blob '$FileName' in container '$StorageContainer'.`n Use -ForceBlob or -Force to overwrite this"
                            continue
                        }
                        $Output = Set-AzureStorageBlobContent @params -File $Localfile -Blob $Filename -ErrorAction Stop -Force
                        if($Output.Name -notlike $Filename)
                        {
                            Throw "Set-AzureStorageBlobContent output seems off:`n$($Output | Format-List | Out-String)"
                        }
                    }
                    Catch
                    {
                        Write-Error $_
                        Write-Error "Failed to generate or upload local script for VM '$VM' in Resource Group '$ResourceGroup'"
                        continue
                    }
                }

                # We have a script in place, set up an extension!
                Write-Verbose "Adding CustomScriptExtension to VM '$VM' in resource group '$ResourceGroup'"
                Try
                {
                    $Output = Set-AzureRmVMCustomScriptExtension -ResourceGroupName $ResourceGroup `
                                                                    -VMName $VM `
                                                                    -Location $AzureRmVM.Location `
                                                                    -FileName $Filename `
                                                                    -Run $Filename `
                                                                    -ContainerName $StorageContainer `
                                                                    -StorageAccountName $StorageAccountName `
                                                                    -StorageAccountKey $StorageAccountKey `
                                                                    -Name $ExtensionName `
                                                                    -TypeHandlerVersion 1.1 `
                                                                    -ErrorAction Stop

                    if($Output.StatusCode -notlike 'OK')
                    {
                        Throw "Set-AzureRmVMCustomScriptExtension output seems off:`n$($Output | Format-List | Out-String)"
                    }
                }
                Catch
                {
                    Write-Error $_
                    Write-Error "Failed to set CustomScriptExtension for VM '$VM' in resource group $ResourceGroup"
                    continue
                }

                # collect the output!
                Try
                {
                    $AzureRmVmOutput = $null
                    $AzureRmVmOutput = Get-AzureRmVM @CommonParams -Status -ErrorAction Stop
                    $SubStatuses = ($AzureRmVmOutput.Extensions | Where {$_.name -like $ExtensionName} ).substatuses
                }
                Catch
                {
                    Write-Error $_
                    Write-Error "Failed to retrieve script output data for $VM"
                    continue
                }

                $Output = [ordered]@{
                    ResourceGroupName = $ResourceGroup
                    VMName = $VM
                    Substatuses = $SubStatuses
                }

                foreach($Substatus in $SubStatuses)
                {
                    $ThisCode = $Substatus.Code -replace 'ComponentStatus/', '' -replace '/', '_'
                    $Output.add($ThisCode, $Substatus.Message)
                }

                [pscustomobject]$Output
            }
        }
    }
}
Try{
    #region Connection to Azure
    write-verbose "Connecting to Azure"
    $connectionName = "AzureRunAsConnection"

    try
    {
        # Get the connection "AzureRunAsConnection "
        $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName         

        "Logging in to Azure..."
        Add-AzureRmAccount `
            -ServicePrincipal `
            -TenantId $servicePrincipalConnection.TenantId `
            -ApplicationId $servicePrincipalConnection.ApplicationId `
            -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 
    }
    catch {
        if (!$servicePrincipalConnection)
        {
            $ErrorMessage = "Connection $connectionName not found."
            throw $ErrorMessage
        } else{
            Write-Error -Message $_.Exception.Message
            throw $_.Exception
        }
    }

    Select-AzureRmSubscription -SubscriptionId $SubscriptionID
    $RG = Get-AzureRmResourceGroup -Name $ResourceGroup
    Foreach($VMName in $VMNames){
        $AzureVM = Get-AzureRmVM -Name $VMName -ResourceGroupName $RG.ResourceGroupName
    
        $Params = @{
            ResourceGroupName = $ResourceGroup
            VMName = $AzureVM.Name
            StorageAccountName = $StorageAccount
            StorageAccountKey = $StorageAccountKey
            StorageContainer = $StorageContainer
            FileName = $ScriptName
            ExtensionName = $ExtensionName
            UploadScript = $UploadScript
        }

        Invoke-AzureRmVmScript @Params -ScriptBlock $ScriptBlock -Force -Verbose
    }
}
Catch{
    Write-Error -Message $_.Exception.Message
    Exit Throw $_
}

Powershell & Graphs

TL;DR I wanted to draw graphs in powershell without any external dependancies. So I did, results below.

How to graph:
Create one or more array of points
Join arrays into another array of lines
Execute Draw-Graph function with some data

#Build graph
[Array]$Line1 = 1..12 | ForEach-Object{Get-Random (10..20)}
$p=Get-Random (14..25)
$p%5
[Array]$Line2 = 1..12 | ForEach-Object{
    If($p%5 -eq 0){
        $p-=(get-random (1..3))
    }
    Else{
        $p+=(get-random (1..5))
    }
    $p
}
[Array]$Lines = $Line1,$Line2
$Legend = "Line1 Header","Line2 Header"
$Colors = "Blue","Green"

$file = ([guid]::NewGuid()).Guid
$file = "$env:TEMP\$file.png"
Draw-Graph -Lines $Lines -Legend $Legend -Colors $Colors -Header "Header" -SaveDestination $file
.$file

Graph functions/Initialization

Add-Type -AssemblyName System.Windows.Forms,System.Drawing

Function Get-Color{
    $rbg = @()

    For($i = 0;$i -le 3;$i++){
        Switch($i){
            #Black
            0{ $rbg += 255}#Get-Random -Minimum 128 -Maximum 255 }
            #RGB
            Default{$rbg += Get-Random -Minimum 0 -Maximum 255}
        }
    }
    Return $rbg
}

Function Draw-Graph{
Param(
    $Width = 1024,
    $Height = 512,
    [Array]$Lines,
    [Array]$Legend,
    [Array]$Colors,
    $Header = "Graph",
    $SaveDestination,
    [Switch]$Preview
)
    Begin{}
    Process{
    If($Preview){
        [Windows.Forms.Form]$Window = New-Object System.Windows.Forms.Form
    
        $Window.Width = $Width
        $Window.Height = $Height

        $Window.Show()
        $Window.Refresh()

        [Drawing.Graphics]$Graph = $Window.CreateGraphics()
    }
    Else{
        $bmp = New-Object Drawing.Bitmap $Width,$Height
        $Graph = [Drawing.Graphics]::FromImage($bmp)
            
    }
    $Graph.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
    $Graph.SmoothingMode = [Drawing.Drawing2D.SmoothingMode]::AntiAlias
    $Graph.TextRenderingHint = [Drawing.Text.TextRenderingHint]::AntiAlias
    $Graph.CompositingQuality = [Drawing.Drawing2D.CompositingQuality]::HighQuality

    $Background = [System.Drawing.Color]::Snow
    $Graph.Clear($Background)

    $TextBrush = New-Object Drawing.SolidBrush([System.Drawing.Color]::FromArgb(255, 0, 212,252))
    $Font = New-object System.Drawing.Font("arial",12)  
    $gridPen = [Drawing.Pens]::LightGray
        
    #Draw Graph area
    $DrawArea = New-Object 'object[,]' 2,2
    
    # X (Width)
    [int]$DrawArea[0,0] = $Width/10
    [int]$DrawArea[0,1] = ($Width-$Width/6)
    # Y (Height)
    [int]$DrawArea[1,0] = $Height/10
    [int]$DrawArea[1,1] = ($Height-$Height/3)

    # Get X bounds
    $xFac = ($Lines | ForEach-Object{$_.Length} | Sort -Descending)[0]-1
    $xInc = ($DrawArea[0,1]-$DrawArea[0,0]+$DrawArea[0,0])/$xFac 

    #Get Y bounds
    $yMax = ($lines | ForEach-Object{$_} | sort -Descending)[0]
    $yFac = ($DrawArea[1,1]-$DrawArea[1,0])/$yMax

    #Draw box
    $Graph.DrawRectangle($gridPen, ($DrawArea[0,0]),($DrawArea[1,0]),($DrawArea[0,1]),($DrawArea[1,1]))

    #Draw Header
    $Textpoint = New-Object System.Drawing.PointF ((($DrawArea[0,1]-$DrawArea[0,0])/2+$DrawArea[0,0]),($DrawArea[1,0]/2))
    $Graph.DrawString($Header,$Font,$TextBrush,$TextPoint)

    #Draw horizontal lines
    $scaleFac = 0.1
    $i = 1
    #Get scale
    While($i -ge 1){
        $scaleFac = $scaleFac*10
        $i = $yMax/$scaleFac
    }
    $scaleFac = $scaleFac/10

    0..($yMax/$scaleFac) | ForEach-Object{
        $y = $DrawArea[1,1]-(($_*$scaleFac)*$yFac)+$DrawArea[1,0]
        $x1 = $DrawArea[0,0]
        $x2 = $DrawArea[0,1]+$DrawArea[0,0]

        $Graph.DrawLine($gridPen,$x1,$y,$x2,$y)
        $thisPoint = New-Object System.Drawing.PointF (($x1-10),($y-15))
        $thisSF = New-object System.Drawing.StringFormat
        $thisSF.Alignment = "Far"
        $Graph.DrawString("$($_*$scaleFac)",$Font,$TextBrush,$thisPoint,$thisSF)
    }

    If($lines[0].Count -le 1){
        $tmp = $Lines
        Remove-Variable Lines
        $Lines = @(0)
        $Lines[0] = $tmp
        Remove-Variable tmp
        $Lines
    }

    #DRAW LINE
    $l = 0
    Foreach($Line in $Lines){
        If($Colors.Count -gt $l){
            $Pen = New-Object Drawing.Pen($Colors[$l])
        }
        Else{
            $rgb = Get-Color
            $Pen = New-object Drawing.Pen([System.Drawing.Color]::FromArgb($rgb[0],$rgb[1],$rgb[2],$rgb[3]))
        }
        $Pen.Width = 2

        #Initiate/Reset Points
        $Points = @()
        $Step = 0

        Foreach($point in $line){
            
            $x = ($xInc*$step)+$DrawArea[0,0]
            $y = $DrawArea[1,1]-($point*$yFac)+$DrawArea[1,0]

            $Points += New-Object System.Drawing.PointF($x,$y)
            $Step++
        }
        $Graph.DrawLines($pen,$Points)

        If($Legend.Count -gt $l){
            $thisLegend = $Legend[$l]
            If($Colors.Count -gt $l){
                $thisBrush = New-Object Drawing.SolidBrush($Colors[$l])
            }
            Else{
                $rgb = Get-Color
                $thisBrush = New-Object Drawing.SolidBrush([System.Drawing.Color]::FromArgb($rgb[0],$rgb[1],$rgb[2],$rgb[3]))
            }
                 
            $y = $DrawArea[1,1]+$DrawArea[1,0]+20
            $x = $DrawArea[0,0]+100*$l
                
            $thisPoint = New-Object System.Drawing.PointF ($x,$y)
            $thisFont = New-Object System.Drawing.Font("arial",12,[System.Drawing.FontStyle]::Bold)
            $Graph.DrawString($thisLegend,$thisFont,$thisBrush,$thisPoint)
        }
        $l++
    }
     
    }
    End{
        
        If($Preview){
            Start-Sleep 10
        }
        Else{
            $bmp.save($SaveDestination)
        }

        Try{$Graph.Dispose()}Catch{}
        Try{$bmp.Dispose()}Catch{}
        Try{$Window.Close()}Catch{}
        Try{$Window.Dispose()}Catch{}
    }
}

Generate-Key

To compliment my earlier post I have created a short function to generate keys.

Function Generate-key{
Param(
    [validateset(128,192,256)]
    $bits,
    [switch]$COUT,
    $seed
)
    $bytes = $bits/8
    $array = @()
    If($seed){
        1..$bytes | ForEach-Object{
            $array += Get-Random -Minimum 0 -Maximum 255 -SetSeed $seed
        }
    }
    Else{
        1..$bytes | ForEach-Object{
            $array += Get-Random -Minimum 0 -Maximum 255
        }
    }
    If($COUT){
        [string]$retval = ($array -join ',') 
        Return $retval
    }
    Else{
        Return [Byte[]]$Key = $array
    }
}

Encrypting strings with custom keys in powershell

Since the ConvertTo-SecureString is not really secure, and neither is the EncodedCommand (base64string) I made two short functions to encrypt and decrypt strings in order to send them across the void unharmed.

Function Encrypt-String{
Param(
    
    [Parameter(
        Mandatory=$True,
        Position=0,
        ValueFromPipeLine=$true
    )]
    [Alias("String")]
    [String]$PlainTextString,
    
    [Parameter(
        Mandatory=$True,
        Position=1
    )]
    [Alias("Key")]
    [byte[]]$EncryptionKey
)
    Try{
        $secureString = Convertto-SecureString $PlainTextString -AsPlainText -Force
        $EncryptedString = ConvertFrom-SecureString -SecureString $secureString -Key $EncryptionKey

        return $EncryptedString
    }
    Catch{Throw $_}

}

this function accepts a string, a key and outputs the encryptedstring.

To encrypt call the function with a plaintext string and a key. the key must be a byte array with 16,24 or 32 bytes (128,192 or 256 bits)

Calling the function looks like this.

$string = @'
I am about to become encrypted!
'@

[Byte[]]$Key = 117,9,103,192,133,20,53,149,81,95,108,34,81,224,226,220,56,68,133,120,139,241,176,239,171,54,231,205,83,57,51,255

$EncryptedString = Encrypt-String -PlainTextString $string -Key $Key

The output becomes

PS> $encryptedString
76492d1116743f0423413b16050a5345MgB8AG8AVABnAGcAagArAFYAUgBoAGkAYwBOAFIAZgBTAGEAQQB1AGsAMQBmAHcAPQA9
AHwAZQBiADIAOAA4ADQAYwA5ADEAMABlAGMAOABmADUAOAAyAGIANQAzADIAOABjADIAOAAxADUAZgAxADYAMgAyAGQAMgA2AGUA
ZQA2ADYANAA2AGUAYQA0AGMAMgBmADMAYgAwADIAMgAxAGQAOQAxADcANwBhADgANwAxADcAZgBjADEANwAxADcANwA1ADgANwBh
ADMAMgA3AGEAMABmADcAYQAzADcAYQBiADgANAAwADkAOQA4ADgANgA1AGYAMwA5ADMANAA3ADkAZgAwAGIAZAA1AGQAZgA2AGEA
MABmADIANwBmADkANQBhAGYAOAAxAGYANgA0ADAAOAAxAA==

Now to the decrypt. This function needs the exact encryptedstring and the exact key that the string was encoded with.

Function Decrypt-String{
Param(
    [Parameter(
        Mandatory=$True,
        Position=0,
        ValueFromPipeLine=$true
    )]
    [Alias("String")]
    [String]$EncryptedString,

    [Parameter(
        Mandatory=$True,
        Position=1
    )]
    [Alias("Key")]
    [byte[]]$EncryptionKey
)
    Try{
        $SecureString = ConvertTo-SecureString $EncryptedString -Key $EncryptionKey
        $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
        [string]$String = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
        [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)

        Return $String
    }
    Catch{Throw $_}

}

To call this function simply input the encryptedString and key and you get the pre-encrypted string as the return value.

PS> Decrypt-String -EncryptedString $EncryptedString -EncryptionKey $Key
I am about to become encrypted!

Both functions accept value by pipeline so a Get-content can be used like this.

Get-Content C:\temp\encrypted.txt | Decrypt-String -EncryptionKey $Key

An overkill version of this is to double encode/encrypt. First convert the string to a base64 (encodedCommand style) then encode it with the key. This really adds no further security except to obfuscate the encrypted package and in case someone gets the key to decrypt they have to convert the encodedcommand to a string from base 64

Function Encrypt-String{
Param(
    
    [Parameter(
        Mandatory=$True,
        Position=0,
        ValueFromPipeLine=$true
    )]
    [Alias("String")]
    [String]$PlainTextString,
    
    [Parameter(
        Mandatory=$True,
        Position=1
    )]
    [Alias("Key")]
    [byte[]]$EncryptionKey
)
    Try{
        $bytes = [System.Text.Encoding]::Unicode.GetBytes($PlainTextString)
        $encodedCommand = [Convert]::ToBase64String($bytes)
        $secureString = Convertto-SecureString $encodedCommand -AsPlainText -Force
        $EncryptedString = ConvertFrom-SecureString -SecureString $secureString -Key $EncryptionKey

        return $EncryptedString
    }
    Catch{Throw $_}

}


Function Decrypt-String{
Param(
    [Parameter(
        Mandatory=$True,
        Position=0,
        ValueFromPipeLine=$true
    )]
    [Alias("String")]
    [String]$EncryptedString,

    [Parameter(
        Mandatory=$True,
        Position=1
    )]
    [Alias("Key")]
    [byte[]]$EncryptionKey
)
    Try{
        $SecureString = ConvertTo-SecureString $EncryptedString -Key $EncryptionKey
        $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
        [string]$String = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
        [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)

        $data = @()
        [convert]::FromBase64String($encodedCommand) | ForEach-Object{
            If($_ -ne 0){
                $data += [char]$_
            }
        }

        Return ($data -join '')
    }
    Catch{Throw $_}

}

Retreive passwords through GPO/GPP with powershell.

There is a known vulnerability in windows where all passwords stored in Group policy preferences are encrypted with a public AES key (link to msdn).

In this post we will go through how to get all the usernames and passwords from group policies in the minimum amount of time with maximum amount of data so we can infiltrate the domain.

What we will need is a fast USB stick loaded with a set of scripts. (attached at the bottom of the post)

First we want to retreive all XML files that may contain passwords. We do this by a query to the SYSVOL. This is the bulk of the time and it is recommended to perform this first if you’re using this on someone else’s computer (perhaps forgot to lock or in a public area).
For this run “Get-GPPXML.ps1”

If there is still time, we can go ahead and encrypt by running “Decrypt-passwords.ps1”, this will save it as a csv file to be opened in excel with the following headers for easy reading.
Domain Usernames Passwords NewNames(might be reset) File ChangedTimestamp

http://saemundsson.se/wp-content/uploads/2015/02/Get-GPPPasswords.zip

Counting in roman

1 to 100 looks like this

I, II, III, IV, V, VI, VII, VIII, VIV, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XVIV, XX, XXI, XXII, XXIII, XXIV, XXV, XXVI, XXVII, XXVIII, XXVIV, XXX, XXXI, XXXII, XXXIII, XXXIV, XXXV, XXXVI, XXXVII, XXXVIII, XXXVIV, XC, XCI, XCII, XCIII, XCIV, XCV, XCVI, XCVII, XCVIII, XCVIV, L, LI, LII, LIII, LIV, LV, LVI, LVII, LVIII, LVIV, LX, LXI, LXII, LXIII, LXIV, LXV, LXVI, LXVII, LXVIII, LXVIV, LXX, LXXI, LXXII, LXXIII, LXXIV, LXXV, LXXVI, LXXVII, LXXVIII, LXXVIV, XC, XCI, XCII, XCIII, XCIV, XCV, XCVI, XCVII, XCVIII, XCVIV, LXC, LXCI, LXCII, LXCIII, LXCIV, LXCV, LXCVI, LXCVII, LXCVIII, LXCVIV, C
Function To-Roman{
Param([Parameter(Mandatory=$true)][int]$int)
    [int]$M = 1000
    [int]$D = 500
    [int]$C = 100
    [int]$L = 50
    [int]$X = 10
    [int]$V = 5
    [int]$I = 1
    If($int -eq 0){Throw "Zero does not exist in roman culture.";Return 404}
    $done = $false
    While($done -eq $false){

        #$Roman = $roman+(addup $int $m "M")
        If($int -ge $m){
            $n = Loopy $int $m
            $int = $n[0]
            1..$n[1] | %{$Roman = $Roman+"M"}
            
        }

        If($int -ge $D){
            $n = Loopy $int $D
            $int = $n[0]
            1..$n[1] | %{$Roman = $Roman+"D"}
        }

        If($int -ge $C){
            $n = Loopy $int $C
            $int = $n[0]
            1..$n[1] | %{$Roman = $Roman+"C"}
            
        }

        If($int -ge $L){
            $n = Loopy $int $L
            $int = $n[0]
            1..$n[1] | %{$Roman = $Roman+"L"}
        }

        If($int -ge $X){
            $n = Loopy $int $X
            $int = $n[0]
            1..$n[1] | %{$Roman = $Roman+"X"}
            
        }

        If($int -ge $V){
            $n = Loopy $int $V
            $int = $n[0]
            1..$n[1] | %{$Roman = $Roman+"V"}
        }

        If($int -ge $I){
            $n = Loopy $int $I
            $int = $n[0]
            1..$n[1] | %{$Roman = $Roman+"I"}
            
        }
        If($roman.Contains("CCCC")){$roman=$Roman.Replace("CCCC","CD")}
        If($roman.Contains("XXXX")){$roman=$Roman.Replace("XXXX","XC")}
        If($roman.Contains("LLLL")){$roman=$Roman.Replace("LLLL","XL")}
        If($roman.Contains("LXXX")){$roman=$Roman.Replace("LXXX","XC")}
        If($roman.Contains("VIV")){$roman=$Roman.Replace("VIV","IX")}
        If($roman.Contains("IIII")){$roman=$Roman.Replace("IIII","IV")}

        If($int -eq 0){$done = $true}
    }
    Return $Roman
}

Function Loopy{
Param(
    [int]$int,
    [int]$number
)
    $notDone = $true
    $loops = 0
    While($notDone){
        If($int-$number -ge 0){$int=$int-$number;$loops++}
        ElseIf($int-$number -lt $number){$notDone = $false}
    }
    return $int,$loops
}

1..1000 | %{"$(To-Roman $_) $_"}


Threaded GUI module

Function Invoke-GUI {
<#
    .SYNOPSIS
        Invoke-GUI creates a customizable threaded GUI for powershell
    .DESCRIPTION
        Creates a GUI from XAML file through Windows Presentation Foundation.
        All XAML data that require changeable data requires a "Name" node which sets the variable name
        Any XML nodes with name will get a variable assigned under $Code. and all subsequent data is accessible from there.
        The main GUI will be assigned $Code.GUI
    .PARAMETER File
        Sets a file with the XAML data used to build the interface.
    .PARAMETER XAML
        Input object in form on an already created XML variable.
    .PARAMETER OnTick
        Code to be executed on each tick
    .PARAMETER InitializationScript
        Code to be performed one time before the GUI shows.
    .PARAMETER TimerInterval
        Timer interval
    .PARAMETER LogFile
        LogFile location
    .Example
        Invoke-GUI -File C:\Application\GUI.xaml
        This loads the GUI without any tick action or custom initialization script.
    .Example
        Invoke-GUI -XAML $XAML -LogFile C:\Application\dump.log
        Loads the GUI from an already compiled XAML object and outputs logs to dump.log
    .Example
        Invoke-GUI -XAML $XAML -OnTick {$Code.MessageBox.Content = "Random number: $(Get-Random)"} -TimerInterval "0:1:0.00" -LogFile C:\Application\dump.log
        As example two but updates a MessageBox variable with a random number once per minute.
    .Example
        Invoke-GUI -XAML $XAML -TimerInterval "0:1:0.00" -LogFile C:\Application\dump.log -OnTick {
            $Code.MessageBox.Content = "Random number: $(Get-Random)"
        } -InitializationScript {
            $Code.Company.Content = "Contoso"
            $Code.Agreement.Value = 0
        }
        As example three but adds some company and agreement information at startup.
#>
[cmdletbinding()]
Param (
    [Parameter(Mandatory=$True,ParameterSetName="File")]
    [String]$File,
    [Parameter(Mandatory=$True,ParameterSetName="XAML")]
    [XML]$XAML,
    [Parameter(Mandatory=$False)]
    [ScriptBlock]$OnTick = {},
    [Parameter(Mandatory=$False)]
    [ScriptBlock]$InitializationScript = {},
    [Parameter(Mandatory=$False)]
    [TimeSpan]$TimerInterval = "0:0:1.00",
    [Parameter(Mandatory=$False)]
    [String]$LogFile
)
    
    If($LogFile){
        $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
        "$Time | Starting GUI script" | Out-File $LogFile -Append
    }
    
    $Script:Code = [hashtable]::Synchronized(@{})
    $Code.TimerInterval = $TimerInterval
    $Code.OnTick = $OnTick
    $Code.InitializationScript = $InitializationScript
    $Code.ParameterSet = $PSCmdlet.ParameterSetName
    If($PSCmdlet.ParameterSetName -eq "File"){
        $Code.XAMLFile = $File
    }
    Else{
        $Code.XAML = $XAML
    }

    If($LogFile){
        $Code.LogFile = $LogFile
    }

    $Script:Runspacehash = [hashtable]::Synchronized(@{})
    $Runspacehash.host = $Host
    $Runspacehash.runspace = [RunspaceFactory]::CreateRunspace()
    $Runspacehash.runspace.ApartmentState = “STA”
    $Runspacehash.runspace.ThreadOptions = “ReuseThread”
    $Runspacehash.runspace.Open() 
    $Runspacehash.psCmd = {Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase}.GetPowerShell()
    $Runspacehash.runspace.SessionStateProxy.SetVariable("code",$Code)
    $Runspacehash.runspace.SessionStateProxy.SetVariable("Runspacehash",$Runspacehash)
    $Runspacehash.psCmd.Runspace = $Runspacehash.runspace
    

    $Runspacehash.Handle = $Runspacehash.psCmd.AddScript({ 

        $Script:Update = {
            # Do stuff on tick here
            .$Code.OnTick
        }

        If($Code.ParameterSet -eq "File"){[XML]$XAML = Get-Content $Code.XAMLFile}
        Else{[XML]$XAML = $Code.XAML}

        # builds gui as $Code.GUI with data from $XAML
        # creates variable for each xml property with the "name" assigned.
        Try {

            [reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null

            $objXMLReader = (New-Object System.Xml.XmlNodeReader $XAML)
            $Code.GUI = [Windows.Markup.XamlReader]::Load($objXMLReader)
        
            $Code.GUI.WindowStartupLocation = "CenterScreen"

            If($Code.LogFile){
                $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                "$Time | Creating variables for XAML elements.." | Out-File $Code.LogFile -Append
            }
            $XAML.SelectNodes("//*[@Name]") | ForEach-Object {
                If($Code.LogFile){
                    $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                    "$Time | Variable created: `$Code.$($_.Name)" | Out-File $Code.LogFile -Append
                }
                $Code."$($_.Name)" = $Code.GUI.FindName($_.Name)
            }
        }
        Catch {
            If($Code.LogFile){
                $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                "$Time | $_" | Out-File $Code.LogFile -Append
            }
            Exit        
        }

        #Timer Event
        $Code.GUI.Add_SourceInitialized({
            $Script:timer = new-object System.Windows.Threading.DispatcherTimer 
            If($Code.LogFile){
                $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                "$Time | Timer interval is: $($Code.TimerInterval)" | Out-File $Code.LogFile -Append
            }
            $timer.Interval = [TimeSpan]$Code.TimerInterval
        
            $timer.Add_Tick({
                $Update.Invoke()
                [Windows.Input.InputEventHandler]{$Code.GUI.UpdateLayout()}
            })
        
            #Start timer
            If($Code.LogFile){
                $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                "$Time | Starting timer" | Out-File $Code.LogFile -Append
            }
            $timer.Start()
            If (-NOT $timer.IsEnabled) {

                If($Code.LogFile){
                    $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                    "$Time | Stopping GUI" | Out-File $Code.LogFile -Append
                }

                $Code.GUI.Close()
            }
        }) 

        $Code.GUI.Add_Closed({
            If($Code.LogFile){
                $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                "$Time | GUI disposed" | Out-File $Code.LogFile -Append
            }
            $timer.Stop()
            $Runspacehash.PowerShell.Dispose()
    
            [gc]::Collect()
            [gc]::WaitForPendingFinalizers()    
        })

        Try{$Code.InitializationScript.Invoke()}
        Catch{
            If($Code.LogFile){
                $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                "$Time | $_" | Out-File $Code.LogFile -Append
            }
        }

        # Does not work to place this in the InitializationScript yet. Need to work around this somehow
        $Code.GUI.Add_MouseLeftButtonDown({
            $This.DragMove()
        })

        
        $Code.GUI.ShowDialog() | Out-Null

    }).BeginInvoke()

    #Give gui time to initialize
    Start-sleep 1
}

Function Update-GUI{
<#
    .SYNOPSIS
        Update-GUI Sends a scriptblock for execution in the GUI thread.
    .DESCRIPTION
        Takes a Scriptblock for execution in the GUI thread.
        It is a simple command (9 rows) but as it is used often converted to a function.
        Code is:
        $Code.GUI.Dispatcher.Invoke([action]{
            Try{.$ScriptBlock}
            Catch{
                If($Code.LogFile){
                    $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                    "$Time | Could not execute command: $ScriptBlock" | Out-File $Code.LogFile -Append
                }
            }    
        })
    .PARAMETER ScriptBlock
        Scriptblock to run inside thread.
    .Example
        Update-GUI -ScriptBlock {$Code.Message.content = "Hello world"}
        Updates the message node content with "hello world"
#>
Param(
    [Parameter(Mandatory=$True)]
    [ScriptBlock]$ScriptBlock
) 
    $Code.GUI.Dispatcher.Invoke([action]{
        Try{.$ScriptBlock}
        Catch{
            If($Code.LogFile){
                $time = get-date -Format "yyyy-MM-dd HH:mm:ss"
                "$Time | Could not execute command: $ScriptBlock" | Out-File $Code.LogFile -Append
            }
        }    
    })
}