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

I'm having a weird problem and maybe someone can help me.

I am trying to get Terminal Services attributes from an Active Directory user using C # on a machine using Windows 10. I do this by running a PowerShell script inside my application, for example:

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: "Throwing" InvokeGet "exception with argument" 1 ": Msgstr" Unknown name. from HRESULT: 0x80020006 (DISP_E_UNKNOWNNAME)) "'

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


The weird thing is that when I run the script from the PowerShell console on my Windows 10 computer, it works! Here is the exact code for 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's the output from PowerShell:

PowerShell output

I also tried running the script in the Visual Studio PowerShell Interactive window and that works too. Here is a screenshot and output of this:
(Censorship Identification)

enter image description here


I found a very similar post for mine here , but the answer provided doesn't work.
The answer to the above post states that the properties have changed the names to:
  • msTSAllowLogon
  • msTSHomeDirectory
  • msTSHomeDrive
  • msTSProfilePath

But when I run my scripts with these property names, I don't get proper information. They don't seem to have the same properties. Here are some screenshots of my user in Active Directory Users and Computers:

SomePerson Prop

You can see the property I am trying to extract above.
When I look at the user's user in the Attribute Editor tab, you can see the msTSProfilePath file and it has a different meaning:

enter image description here

Running the script with msTSProfilePath

returns the property specified above in the attribute editor window ( ????????

).


Additional Information:

I tested this against two separate Active Directory domains:

  • One with Windows Server 2012 forest and domain function levels with a DC running on a server that is running Windows Server 2012 version 6.2 (Build 9200)
  • The second forest and domain level tier is Windows Server 2012 R2 and runs on Windows Server 2012 R2 version 6.3 (Build 9600)

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

Thank!

+3


source to share


2 answers


This problem has really bothered my life since since the days of server 2000 I have been trying to find ways to get this value. I'm not sure why the PowerShell script works on Windows 10 (I don't have a win10 window associated with a domain, which I can freely query for testing), but I found a solution to this problem. I know you won't like it at first, but you don’t need anything with it, you don’t need to do anything other than copy / paste and add a short list of the commands I have listed.

I'm sure you've figured out by now that the value is buried in the AD userParameters attribute. This is the encoded value. You can see the spec for unencode this value here (just if you're curious, no need to get your value) https://msdn.microsoft.com/en-us/library/ff635169.aspx

Someone who understands this mess better than I wrote I had a script that does the hard work for us. This script is 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 creates one object called TSuserParameters. You will use a method on this object named .UnBlob in the userParameters data returned from AD. Here I am retrieving this data using Get-ADUser:

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

      

The object will parse the data and store it in the TSAttributes property collection. This trainee stores a CtxWFProfilePath that has the required data. There is metadata stored in the path itself (eg length, since this value is variable width. To see why, read the documentation in the first link, again not related to getting your data). Of course there is metadata stored in the object, you only need the first object in the property array [0]:

$TSuserParameters.TSAttributes.CtxWFProfilePath[0]

      

And now you have the data you want. This SHOULD work back in 2000 as this encoding specification doesn't seem to have changed since then.



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


Here is the complete script Stack package ready to give me a message:

$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 that deep down they tell you, "But PowerShell works!" Even though I cannot verify this, I have the opportunity to work with my experience with MS Orchestrator and PowerShell 1.0. I suspect that if a locally running PowerShell instance can get this data using the script you posted above, and somehow a workflow in C # cannot, you should be able to use Invoke-Command to break the constraint (read: created via C # and somehow broken) fall back to the "full" functional version by wrapping your entire 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 goes to a new session on the local machine with whatever defaults are. I've been doing this all the time, being forced into PowerShell 1.0 from Orchestrator. You can go ahead and run the command directly on the domain controller via -computername if it is not running locally.

If it turns out that this doesn't work, I would very much doubt that the script you are using locally relies on something cached on your local system. This part is just a guess.

+5


source


The script posted by @Ty Savercool above is correct to fetch and decode the userParameters blob, but it has a few bugs that prevent you from modifying properties and re-blobbing userParameters and committing them back to Active Directory.

I am going to post the fixes for anyone who may come across this in the future and must use this script.


  • The first fix is ​​on line 2 of the script, where property types and values ​​are declared. It can be completely dependent on who the AD domain is associating with, if this property even appears but the property is missing CtxProfilePathW

    . If your AD domain does indeed have this property in the userParameters block and it is not declared here, this value will be left blank, and repeated blobbing of userParameters will push all values ​​above one, causing the userParameters value to be corrupted. Here is the corrected line:

    $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"}
    
          

  • The next problem is in the AddUpdate

    script method that allows you to change the values ​​of the userParameters values. In the middle of the method, the parameter variable $NewAttValue

    changes to $AttValue

    . $AttValue

    is never declared, so it is null and the first loop that tries to access the length NewAttValue

    to update the value is called with $AttValue

    .

  • In the method AddUpdate

    ; in the ASCII

    operator logic switch

    on line 5 of the method. A loop that loops over characters $NewAttValue

    will continuously overwrite the first character without moving on to the next characters. This is fixed by adding a temporary variable that takes the character at the correct position $NewAttValue

    , converts it to hex, and then adds to the variable $HexString

    , which is the new value of the attribute.

  • If a new userParameters attribute is added, the attribute counter must be incremented in the method AddUpdate

    . This is done by adding $this.AttributeCount += 1

    at the very end of the method after adding the new attribute to the attribute list.

  • The last problem is related to the AddUpdate method. If the Int32

    Hex value and representation is less than 8 bytes, it mangles the userParameters object. All of these values ​​must be 8 bytes long, so zeros must be added to it. This is done with this line of code inserted into the Int32

    statement logic swtich

    :
    while ($hexstring.length -lt 8){ $hexstring = "0" + $hexstring}

Below is the method AddUpdate

with fixes 2 - 5:

$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, you can get AD Terminal Services properties, decode, update, and recode them seamlessly!

Below is an example of using a script to contact 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 problems with the values Int32

that are returned from this script. When you print them directly, they don't seem to make any sense; For example, CtxMaxIdleTime

, CtxMaxDisconnectionTime

and CtxMaxConnectionTime

should withdraw its value in a matter of minutes. When you print them directly from the script, they show up with strange meanings:

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 values ​​are stored in network bytes (big-endian) and in Windows x86 architecture, they are read in little-endian order.

Here is a link for more information on endianness.

To read these values, we need to convert the values ​​from large to little-endian.

Below is the code to do this using the values CtxMaxIdleTime

, CtxMaxDisconnectionTime

and CtxMaxConnectionTime

. 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 the values ​​in minutes back to the correct values, here's 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

      

+1


source







All Articles