Unable to get Active Directory Terminal Services attributes while running PowerShell script in Visual Studio

3

I have run into a strange problem, and maybe someone can help me.

I am attempting to retrieve the Terminal Services attributes from an Active Directory user using C# on a machine using Windows 10. I am doing this by running a PowerShell script inside my application like so:

var script = $@"Import-module ActiveDirectory
                $user=[ADSI]""LDAP://192.111.222.33:389/CN=SomePerson,DC=Domain,DC=local""
                $user.psbase.Username = ""administrator""
                $user.psbase.Password = ""adminPassword""                 
                $user.psbase.invokeget(""TerminalServicesProfilePath"")";

using (var runspace = RunspaceFactory.CreateRunspace())
        {
            runspace.Open();
            using (var pipeline = runspace.CreatePipeline())
            {
                pipeline.Commands.AddScript(script);
                var test = pipeline.Invoke();
                Console.WriteLine("Success: ");
                return true;
            }
        }

I am getting this exception:

System.Management.Automation.MethodInvocationException: 'Exception calling "InvokeGet" with "1" argument(s): "Unknown name. (Exception from HRESULT: 0x80020006 (DISP_E_UNKNOWNNAME))"'

When I run the above code in Visual Studio 2015 on a machine using Windows Server 2012 as the OS it works just fine! I made sure that my Windows 10 machine has RSAT installed as well.


What is strange is that when I run the script from a PowerShell console on my Windows 10 machine, it works! Here is the exact code of my PowerShell script:

$user = [ADSI]"LDAP://192.111.222.33:389/CN=SomePerson,DC=Domain,DC=local"
$user.psbase.Username = "administrator"
$user.psbase.Password = "adminPassword"
Write-Output "Terminal Services profile path:"
Write-Output $user.psbase.invokeget("TerminalServicesProfilePath")

And here is the output from PowerShell:

PowerShell output

I also tried running the script in Visual Studio's PowerShell Interactive Window, and that works as well. Here is a screenshot and the output of that:
(Identifying info censored)

enter image description here


I found a very similar post to mine Here, but the provided answer does not work.
The answer to the above post states that the properties have changed names to:

  • msTSAllowLogon
  • msTSHomeDirectory
  • msTSHomeDrive
  • msTSProfilePath

But when I run my scripts with those property names, I do not receive the proper information back. They seem to not be the same properties. Here are a couple of screenshots of my user in Active Directory Users and Computers:

SomePerson Prop

You can see the property that I am attempting to retrieve above.
When I look at the attibutes of the user on the Attribute Editor tab, you can see msTSProfilePath, and it holds a different value:

enter image description here

Running the script with msTSProfilePath returns the property seen above in the Attribute Editor window (????????).


Additional Info:

I have tested this against two separate Active Directory Domains:

  1. One with Forest and Domain function levels of Windows Server 2012 with the DC running on a server with Windows Server 2012 Version 6.2 (Build 9200)
  2. The second with Forest and Domain Function levels of Windows Server 2012 R2 and running on Windows Server 2012 R2 Version 6.3 (Build 9600)

I ran this code on another Windows 10 machine and the problem persists.

Thank you!

c#
visual-studio
powershell
active-directory
asked on Stack Overflow Jul 14, 2017 by Robyn Cute • edited Jul 18, 2017 by Robyn Cute

2 Answers

6

This problem really bothered the life out of me as since the days of server 2000 I have been trying to find ways to get at this value. I am unsure why the PowerShell script works on Windows 10 (I don't have a win10 box connected to a domain that I can freely query to test) but I did find a resolution to this problem. I know that you are not going to like it at first but bare with it, you don't have to do anything but copy/paste and append the short list of commands I have listed.

I'm sure you have figured out by now that the value is buried in the userParameters AD attribute. This is an encoded value. You can see the specification to unencode this value here (only if you are interested, not needed to get your value) https://msdn.microsoft.com/en-us/library/ff635169.aspx

Someone that understands this mess better then I do wrote I script that does the hard work for us. This script is located in the third post here: https://social.technet.microsoft.com/Forums/scriptcenter/en-US/953cd9b5-8d6f-4823-be6b-ebc009cc1ed9/powershell-script-to-modify-the-activedirectory-userparameters-attribute-to-set-terminal-services?forum=ITCG

Just copy and paste all the code into your PowerShell script. It only builds 1 object called TSuserParameters. You will use a method off that object called .UnBlob on the userParameters data returned from AD. Here I pull this data with Get-ADUser:

$TSuserParameters.UnBlob((get-aduser -Identity someuser -Properties userparameters).userparameters) 

The object will parse the data and store it under the TSAttributes property collection. This intern stores the CtxWFProfilePath which has the data you want. There is meta data stored with the path itself (e.g. the length as this value is a variable width. To understand why, read the documentation in the first link, again, not relevant to getting your data). Becuase there is metadata stored with the object you will only want the first object in the property array [0]:

$TSuserParameters.TSAttributes.CtxWFProfilePath[0]

And now you have the data you want. This SHOULD work as far back as server 2000 as this specification of encoding does not appear to have changed since then.

You will also have access to the other TS attributes with this object.


Here is the full script is Stack is willing to let me post it:

$TSuserParameters = New-Object System.Object
$TSuserParameters |Add-Member -membertype NoteProperty -name Types -value @{"CtxCfgPresent" = "Int32"; "CtxCfgFlags1" = "Int32"; "CtxCallBack" = "Int32"; "CtxKeyboardLayout" = "Int32"; "CtxMinEncryptionLevel" = "Int32"; "CtxNWLogonServer" = "Int32"; "CtxWFHomeDirDrive" = "ASCII"; "CtxWFHomeDir" = "ASCII"; "CtxWFHomeDrive" = "ASCII"; "CtxInitialProgram" = "ASCII"; "CtxMaxConnectionTime" = "Int32"; "CtxMaxDisconnectionTime" = "Int32"; "CtxMaxIdleTime" = "Int32"; "CtxWFProfilePath" = "ASCII"; "CtxShadow" = "Int32"; "CtxWorkDirectory" = "ASCII"; "CtxCallbackNumber" = "ASCII"}
$TSuserParameters |Add-Member -membertype NoteProperty -name TSAttributes -value @{}
$TSuserParameters |Add-Member -membertype NoteProperty -name SpecificationURL -value"http://msdn.microsoft.com/en-us/library/cc248570(v=prot.10).aspx"
$TSuserParameters |Add-Member -membertype NoteProperty -name Reserved -value [byte[]]
$TSuserParameters |Add-Member -membertype NoteProperty -name AttributeCount -value [uint16]0
$TSuserParameters |Add-Member -membertype ScriptMethod -name init -value {
    $this.TSAttributes = @{}
    [byte[]]$this.Reserved = [byte[]]$null
}
$TSuserParameters |Add-Member -membertype ScriptMethod -name UnBlob -value {
    Param ($Input)
    $this.init()
    $ArrayStep = 1
    #Add-Type -AssemblyName mscorlib
    #A new array for writing things back
    [Byte[]] $resultarray = $NULL
    #$userInfo.userParameters
    $char = [char]1
    #The value is a binary blob so we will get a binary representation of it
    #$input.length
    $userparms = [System.Text.Encoding]::unicode.GetBytes($Input)
    #$userparms.count
    #$userInfo.userParameters
    If ($userparms) #if we have any data then we need to process it
    {
        #Write-Host "Processing $userparms"
        $Valueenum = $userparms.GetEnumerator()
        $Valueenum.reset()
        $result = $Valueenum.MoveNext()
        #Now lets get past the initial reserved 96 bytes as we do not care about this.
        Write-Host "skipping reserved bytes"
        for ($ArrayStep = 1; $ArrayStep -le 96; $ArrayStep ++)
        {
            [byte[]]$this.reserved += $Valueenum.current #Store the reserved section so we can add it back for storing
            #Write-Host "Step $ArrayStep value $value"
            $result = $Valueenum.MoveNext()
        }
        #Next 2 bytes are the signature nee to turn this into a unicode char and if it is a P there is valid tem services data otherwise give up
        #So to combine two bites into a unicode char we do:
        Write-Host "Loading signature"
        [Byte[]]$unicodearray = $NULL
        for ($ArrayStep = 1; $Arraystep -le 2; $ArrayStep ++)
        {
            $value = $Valueenum.current
            #Write-Host "Step $ArrayStep value $value"
            [Byte[]]$unicodearray += $Value
            $result = $Valueenum.MoveNext()
        }
        $TSSignature = [System.Text.Encoding]::unicode.GetString($unicodearray)
        Write-Host "Signatire is $TSSignature based on $unicodearray"
        [uint32] $Value = $NULL
        If ($TSSignature -eq "P") # We have valid TS data
        {
            Write-Host "We have valid TS data so process it"
            #So now we need to grab the next two bytes which make up a 32 bit unsigned int so we know how many attributes are in this thing
            #We have no such data type so lets improvise by adding the value of the bytes together after multiplying the higer order byte by 256
            $Value = [uint16]$Valueenum.current
            $result = $Valueenum.MoveNext()
            $Value += [uint16]$Valueenum.current * 256
            $result = $Valueenum.MoveNext()
            write-Host "Found $value TS Attributes in the blob"
            $this.AttributeCount = [uint16]$value
            For ($AttribNo = 1; $AttribNo -le $value; $AttribNo ++)#For each attribute lets get going
            {
                #Get the first attribute, 2 bytes for name length, 2 bytes for value length, and 2 bytes for type, followed by the data. 
                #Grab name length
                $NameLength = [uint16]$Valueenum.current
                $result = $Valueenum.MoveNext()
                $NameLength += [uint16]$Valueenum.current * 256
                $result = $Valueenum.MoveNext()

                #Grab Value length
                $ValueLength = [uint16]$Valueenum.current
                $result = $Valueenum.MoveNext()
                $ValueLength += [uint16]$Valueenum.current * 256
                $result = $Valueenum.MoveNext()
                #Grab Type
                $TypeValue = [uint16]$Valueenum.current
                $result = $Valueenum.MoveNext()
                $TypeValue += [uint16]$Valueenum.current * 256
                $result = $Valueenum.MoveNext()
                #Write-Host "NameLength is $NameLength, ValueLength is $ValueLength, Type is $TypeValue"
                #Now we know how many bytes bellong to the following fields:
                #Get the name bytes into an array
                $NameUnicodeArray = $NULL
                for ($ArrayStep = 1; $Arraystep -le $NameLength; $ArrayStep ++)
                {
                    [Byte[]]$NameUnicodeArray += $Valueenum.current
                    $result = $Valueenum.MoveNext()
                }
                #Get the attribute value bytes into an array
                $ATTValueASCIICodes = ""
                for ($ArrayStep = 1; $Arraystep -le $ValueLength; $ArrayStep ++)
                {
                    $ATTValueASCIICodes += [char][byte]$Valueenum.current
                    $result = $Valueenum.MoveNext()
                }
                #Grab the name
                $AttributeName = [System.Text.Encoding]::unicode.GetString($NameUnicodeArray)
                Write-Host "UnBlobing: $AttributeName"
                #manipulate the value array as required
                #it is sets of two ASCII chars representing the numeric value of actual ASCII chars
                $AttributeValue = $NULL
                #$TempStr = "" #tem string for the Hex values
                #$ValueByteArray | foreach {    $TempStr += [char][byte]$_ } #get the bytes into a string as the ASCII chars
                #write-host "Temp String = $ATTValueASCIICodes it is $($ATTValueASCIICodes.length)"
                switch ($this.Types.$AttributeName)
                {
                    "Int32" {               
                        $AttributeValue = [convert]::toint32($ATTValueASCIICodes,16)
                    }
                    "ASCII" {
                        $AttributeValue = ""
                        #$ASCIIString = [System.Text.Encoding]::ASCII.GetString($TempStr)# make them into an ascii string
                        for ($ArrayStep = 0; $Arraystep -lt $ATTValueASCIICodes.length; $ArrayStep += 2)
                        {
                            $FinalChar = [char][byte]([convert]::toint16( $ATTValueASCIICodes[($ArrayStep) ] + $ATTValueASCIICodes[$ArrayStep + 1],16)) #Grab the char created by this conversion
                            $AttributeValue += $FinalChar #add them to the array.
                        }
                    }
                    Default {
                        $AttributeValue = "Attribute type Not defined"
                    }
                }

                If ($this.TSAttributes.containsKey($AttributeName))
                {
                    $this.TSAttributes.$AttributeName = @($AttributeValue,$ATTValueASCIICodes,$NameLength,$ValueLength,$TypeValue)
                }
                else
                {
                    $this.TSAttributes.add($AttributeName,@($AttributeValue,$ATTValueASCIICodes,$NameLength,$ValueLength,$TypeValue))
                }
            }
            Write-Host "================================"
        }
        else
        {
            write-host "Signature is not valid, no TS Data"
        }
    }
}
$TSuserParameters |Add-Member -membertype ScriptMethod -name Blobify -value {
    #Lets build this thing
    #Start with the reserved bytes
    [byte[]]$result = $this.Reserved
    #now add the Signature "P" as we are writing valid data
    [byte[]]$result += [System.Text.Encoding]::unicode.GetBytes("P")
    #Now for the number of attributes being stored, we need to reverse the bytes in this 16 bit unsigned int
    $byte1 = [byte](($this.AttributeCount -band 65280) % 256)
    $byte2 = [byte]($this.AttributeCount -band 255)
    [byte[]]$result += $byte2
    [byte[]]$result += $byte1
    #Now for the attributes:
    $this.TSAttributes.getenumerator() | foreach {
        $Valuearray = $_.value
        $attname = $_.key
        #Get the reversed bytes for the NameLength field
        $byte1 = [byte](($Valuearray[2] -band 65280) % 256)
        $byte2 = [byte]($Valuearray[2] -band 255)
        [byte[]]$result += $byte2
        [byte[]]$result += $byte1
        #And again for the ValueLength
        $byte1 = [byte](($Valuearray[3] -band 65280) % 256)
        $byte2 = [byte]($Valuearray[3] -band 255)
        [byte[]]$result += $byte2
        [byte[]]$result += $byte1
        #And again for the typevalue
        $byte1 = [byte](($Valuearray[4] -band 65280) % 256)
        $byte2 = [byte]($Valuearray[4] -band 255)
        [byte[]]$result += $byte2
        [byte[]]$result += $byte1
        #Now add the propertyname in plain ASCII text
        Write-Host "Blobifying `"$attname`""
        #$attnamearray = [System.Text.Encoding]::unicode.GetBytes("$attname")
        #Write-Host "Attname array = $($attnamearray.count), valuelength = $($Valuearray[2])"
        [byte[]]$result += [System.Text.Encoding]::unicode.GetBytes("$attname")
        #write-Host "$($result.count)"
        #for ($loopcount = 1; $loopcount -le $attname.length; $loopcount ++)
        #{
        #   [byte[]]$result += [BYTE][CHAR]$attname[$loopcount - 1]
        #}
        #And finaly add the value to the result using the ASCII conversion
        #New array of bytes to add  the att value to so we can see how big it is
        $HexString = $Valuearray[1]
        [byte[]]$attvalbytes = $null
        switch ($this.Types.$attname)
        {
            "ASCII" {
                #now for each part of the hex string lets get the value for that ascii char
                $HexString.ToCharArray() | foreach {
                    [byte[]]$attvalbytes += [BYTE][CHAR]($_)
                }
            }
            "Int32" {
                #For each char we need to store the byte value
                $HexString.ToCharArray() | foreach {
                    [byte[]]$attvalbytes += [BYTE][CHAR]($_ )
                }
            }
        }
        $result += $attvalbytes
        write-Host "att value is $($attvalbytes.count) and was $($Valuearray[3])"
        Write-Host "NewASCII = $([System.Text.Encoding]::ASCII.GetString($attvalbytes))"
        Write-Host "OldASCII = $($Valuearray[1])"
        Write-Host "================================"
        #[System.Text.Encoding]::unicode.GetString($result)
    }
    return [System.Text.Encoding]::unicode.GetString($result)
}
$TSuserParameters |Add-Member -membertype ScriptMethod -name AddUpdate -value {
    Param ($Attname,$NewAttValue,$TypeValue)
    $HexString = ""

    switch ($this.Types.$Attname)
    {
        "ASCII" {
            Write-host "ascii"
            for ($loopcount = 0; $loopcount -lt $AttValue.length; $loopcount ++)
            {
                #Lets get the Hex value for this char as a string
                $HexString = [convert]::tostring([BYTE][CHAR]($AttValue[$loopcount]),16)
                #As the hex conversion drops the leading zero on the first char if we have a less than 10 value add 0 toi the front if we do not have an even number of chars
                If (($hexstring.length % 2) -eq 1){ $hexstring = "0" + $hexstring}
            }
        }
        "Int32" {
            #convert the int32 to hex
            $HexString = [convert]::tostring($AttValue,16)
            #As the hex conversion drops the leading zero on the first char if we have a less than 10 value add 0 toi the front if we do not have an even number of chars
            If (($hexstring.length % 2) -eq 1){ $hexstring = "0" + $hexstring}
            #There is also the special case of the ctX flags value which is always stored as the full 32bits even when there ere empty bits:
            if (($attname -eq "CtxCfgFlags1") -and ($hexstring.length -lt 8))
            {
                $Loopmax = $hexstring.length
                for ($loopcount = 1; $loopcount -le (8 - $Loopmax); $loopcount ++) {$HexString = "0" + $HexString ; Write-host "Done"}
            }
        }
    }
    $namelenght = ([System.Text.Encoding]::unicode.GetBytes($Attname)).count
    #Now change the values in the table:
    If ($this.TSAttributes.containsKey($Attname))
    {
        #If we do not have an type value we can look in the table and get it from there as it is unlikely this attribute will change types
        If (-not $TypeValue)#If we are not offered a type value by the user we will assum it is the standard of 1
        {
            $TypeValue = $this.TSAttributes.$Attname[4]
        }
        $this.TSAttributes.$Attname = @($NewAttValue,$HexString,$namelenght,$HexString.length,$TypeValue)
    }
    else
    {
        If (-not $TypeValue)#If we are not offered a type value by the user we will assum it is the standard of 1
        {
            $TypeValue = 1
        }
        $this.TSAttributes.add($Attname,@($NewAttValue,$HexString,$namelenght,$HexString.length,$TypeValue))
    }
}
$TSuserParameters |Add-Member -membertype ScriptMethod -name Remove -value {
    Param ($Attname)
    If ($this.TSAttributes.containsKey($Attname))
    {
        $test.remove("12")
        return $true
    }
    else
    {
        return $false
    }
}

I know deep down your gut is telling you "But PowerShell works!". Even though I cannot test this, I have a possible work around from my experience with MS Orchestrator and PowerShell 1.0. I suspect that IF a locally started PowerShell instance can get this data via the script you posted above and somehow the workflow in C# cannot then you should be able to use Invoke-Command to break out of the restricted (read: instantiated via C# and somehow broken) version back to a "full" feature version by wrapping your whole script in a variable and passing it to

invoke-command -computername localhost -scriptblock $yourScriptVar

This logic only executes the invoke-command under the C# interpreter and then passes off to a fresh session on the local machine with whatever defaults are in place. I used to do this all the time when being forced into PowerShell 1.0 from Orchestrator. You could go a step further and run the command directly on the domain controller via -computername if it doesn't work locally.

If it turns out that this does not work then I would be highly suspect that the script you are using locally is relying on something cached on your local system. This part is purely a hunch.

answered on Stack Overflow Jul 19, 2017 by Ty Savercool • edited Jul 19, 2017 by Ty Savercool
1

The script posted by @Ty Savercool above is correct for retrieving and decoding the userParameters blob, but there are a couple of errors in it that don't allow you to change the properties and re-blobb the userParameters and commit them back to Active Directory.

I'm going to post the fixes here for anyone who may come across this in the future and need to use this script.


  1. The first fix is in line 2 of the script where the property types and values are declared. This may be entirely dependent on the AD domain being contacted as to if this property even comes up, but it is missing the property CtxProfilePathW. If your AD domain does indeed have this property in the userParameters blob and it is not declared here, that value will be left blank, and re-blobbing the userParameters will shift all values over one causing the userParameters value to be corrupt. Here is the line fixed:

    $TSuserParameters |Add-Member -membertype NoteProperty -name Types -value @{"CtxCfgPresent" = "Int32"; "CtxCfgFlags1" = "Int32"; "CtxCallBack" = "Int32"; "CtxKeyboardLayout" = "Int32"; "CtxMinEncryptionLevel" = "Int32"; "CtxNWLogonServer" = "Int32"; "CtxWFHomeDirDrive" = "ASCII"; "CtxWFHomeDir" = "ASCII"; "CtxWFHomeDrive" = "ASCII"; "CtxInitialProgram" = "ASCII"; "CtxMaxConnectionTime" = "Int32"; "CtxMaxDisconnectionTime" = "Int32"; "CtxMaxIdleTime" = "Int32"; "CtxWFProfilePath" = "ASCII"; "CtxWFProfilePathW" = "ASCII";"CtxShadow" = "Int32"; "CtxWorkDirectory" = "ASCII"; "CtxCallbackNumber" = "ASCII"}
    
  2. The next issue is in the AddUpdate method of the script, which allows you to change the values of the userParameters values. Halfway through the method, the parameter variable $NewAttValue gets changed to $AttValue. $AttValue is never declared, so it is null and the loop the first loop that tries to access the length of NewAttValue to update the value is called with $AttValue.

  3. In the AddUpdate method as well; in the ASCII logic of the switch statement on line 5 of the method. The loop that loops over the characters of the $NewAttValue continuously overwrites the first character, never advancing to the next characters. This is fixed by adding a temporary variable that takes the character at the correct position of the $NewAttValue, converts it to hex, then appends it to the $HexString variable that is the new value of the attribute.

  4. If a new attribute is added to the userParameters blob, the count of the attributes needs to be incremented in the AddUpdate method. This is done by adding $this.AttributeCount += 1 at the very end of the method after the new attribute is added to the list of attributes.

  5. The last issue is with the AddUpdate method as well. If the value is Int32 and the Hex representation of it is less than 8 bytes, it corrupts the userParameters object. All of these values need to be 8 bytes long, so zeros need to be added to it. This is done with this line of code inserted into the Int32 logic of the swtich statement:
    while ($hexstring.length -lt 8){ $hexstring = "0" + $hexstring}

Below is the AddUpdate method with fixes 2 - 5 applied:

$TSuserParameters |Add-Member -membertype ScriptMethod -name AddUpdate -value {
    Param ($Attname,$NewAttValue,$TypeValue)
    $HexString = ""

    switch ($this.Types.$Attname)
    {
        "ASCII" {
            Write-host "ascii"
            Write-Output $NewAttValue.length
            for ($loopcount = 0; $loopcount -lt $NewAttValue.length; $loopcount ++)
            {
                #Lets get the Hex value for this char as a string
                $TempHexString = [convert]::tostring([BYTE][CHAR]($NewAttValue[$loopcount]),16)
                #As the hex conversion drops the leading zero on the first char if we have a less than 10 value add 0 toi the front if we do not have an even number of chars
                If (($TempHexString.length % 2) -eq 1){ $TempHexString = "0" + $TempHexString}
                $HexString += $TempHexString
            }
        }
        "Int32" {
            #convert the int32 to hex
            $HexString = [convert]::tostring($NewAttValue,16)
            #As the hex conversion drops the leading zero on the first char if we have a less than 10 value add 0 toi the front if we do not have an even number of chars
            If (($hexstring.length % 2) -eq 1){ $hexstring = "0" + $hexstring}
            while ($hexstring.length -lt 8){ $hexstring = "0" + $hexstring}
            #There is also the special case of the ctX flags value which is always stored as the full 32bits even when there ere empty bits:
            if (($attname -eq "CtxCfgFlags1") -and ($hexstring.length -lt 8))
            {
                $Loopmax = $hexstring.length
                for ($loopcount = 1; $loopcount -le (8 - $Loopmax); $loopcount ++) {$HexString = "0" + $HexString ; Write-host "Done"}
            }
        }
    }
    $namelenght = ([System.Text.Encoding]::unicode.GetBytes($Attname)).count
    #Now change the values in the table:
    If ($this.TSAttributes.containsKey($Attname))
    {
        #If we do not have an type value we can look in the table and get it from there as it is unlikely this attribute will change types
        If (-not $TypeValue)#If we are not offered a type value by the user we will assum it is the standard of 1
        {
            $TypeValue = $this.TSAttributes.$Attname[4]
            Write-Output $TypeValue
        }

        $this.TSAttributes.$Attname = @($NewAttValue,$HexString,$namelenght,$HexString.length,$TypeValue)
        Write-Output $this.TSAttributes.$Attname
    }
    else
    {
        If (-not $TypeValue)#If we are not offered a type value by the user we will assum it is the standard of 1
        {
            $TypeValue = 1
        }
        $this.TSAttributes.add($Attname,@($NewAttValue,$HexString,$namelenght,$HexString.length,$TypeValue))
        $this.AttributeCount += 1
    }
}

With these fixes applied, you can retrieve an AD user's terminal services properties, decode, update, and re-encode them with no issue!

Here is an example of how to use the script to do that contacting a remote server:

$password = ConvertTo-SecureString "adminPassword" -AsPlainText -Force
$cred= New-Object System.Management.Automation.PSCredential ("Domain\administrator", $password)

$TSuserParameters.UnBlob((get-aduser -Server "192.111.222.33:389" -credential $cred -Identity SomePerson -Properties userparameters).userparameters)

$TSuserParameters.AddUpdate("CtxWFProfilePath","New Profile Path!") 

Parameters = $TSuserParameters.Blobify($TSuserParameters.TSAttributes)
Write-Output $Parameters

Set-ADUser -Server "192.111.222.33:389" -credential $cred -Identity SomePerson -Replace @{userParameters=$Parameters}


EDIT

I also found some issues with the Int32 values that are returned from this script. When you print them out directly, they appear to not make any sense; For instance, the CtxMaxIdleTime, CtxMaxDisconnectionTime, and CtxMaxConnectionTime are supposed to output their values in minutes. When you print them directly from the script, they appear with strange values like so:

Time         Output from Script
1 minute:    1625948160
5 minuites:  -527236096     
10 minutes:  -1071183616
15 minutes:  -1598354176
30 minutes:  1081547520
1 hour:      -2131872256

This is because the values are stored in Network Byte Order (big-endian) and in Windows x86 architecture, they are read in little-endian order.

Here is a link with more information on endianness.

In order to read these values, we need to convert the values from big-endian order to little-endian.

Below is the code to do that with the CtxMaxIdleTime, CtxMaxDisconnectionTime, and CtxMaxConnectionTime values. This outputs the values in minutes:

# Get the Hex value of the number and split it into groups of two
$realvalue = ($TSuserParameters.TSAttributes.CtxMaxConnectionTime[1] -split '(..)' | ? { $_ })
# Reverse the order of the Hex values into little-endian
$new = $realvalue[3] += $realvalue[2] += $realvalue[1] += $realvalue[0]
# Convert the new hex string to decimal, and divide by nanoseconds to get minutes
$new1 = ([convert]::toint32($new,16)/60000)
Write-Output $new1

To convert values in minutes back to the correct values, here is how to do it:

# Convert the minutes into nanoseconds and convert to hex 
$HexString = [convert]::tostring(($new1*60000),16)            
# Make sure the new hex string is 8 bytes long
while($HexString.length -lt 8){ $hexstring = "0" + $hexstring}
# Split the hex string into groups of two characters
$realvalue = ($HexString -split '(..)' | ? { $_ })
# Reverse the order of the hex characters back to big-endian
$new = $realvalue[3] += $realvalue[2] += $realvalue[1] += $realvalue[0]
# Convert to decimal
$actual = ([convert]::toint32($new,16))
Write-Output $actual
answered on Stack Overflow Jul 24, 2017 by Robyn Cute • edited Oct 4, 2017 by Robyn Cute

User contributions licensed under CC BY-SA 3.0