Finding a way to prevent RDP sessions using unconventional methods that other people cannot figure out is magical, I discovered a powerful but hidden feature which is usually undocumented that allows bypassing the standard Remote Desktop Users group entirely. This "ninja" method uses custom RDP-Tcp permissions stored directly in the registry, giving you granular control over who can access your servers via Remote Desktop Protocol (RDP).
What Are RDP-Tcp Custom Permissions?
Most administrators are familiar with the standard approach: add users to the "Remote Desktop Users" group, and they can RDP to the server. However, Windows has a lesser-known capability to define custom Access Control Lists (ACLs) for the RDP-Tcp connection that completely bypass group membership.
These custom permissions are stored in a specific registry location:
HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp\
The magic happens in a registry value called "Security" - a binary blob that contains the ACL determining who can access the server.
Creating the Baseline: You Need a Legacy Server
Here's the catch: modern Windows servers don't easily allow you to create these custom permissions through the GUI. You need access to a Windows Server 2008 system with the Remote Desktop Session Host role installed.
On Server 2008, you can use the Terminal Services Configuration Manager to:
- Navigate to the RDP-Tcp connection properties
- Go to the Security tab
- Add some random groups or users to create the initial custom ACL - this is required as the script cannot write new values but only override new ones!!!!
- This writes the binary ACL data to the registry
Once you have this baseline, you can export the registry data and use it as a template for other servers, to complete this export the key from the registry location
HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp\
You will then need to remove all the values excluding the "Security" key which should be in Hex as below, this is the only key to remain.
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp]
"Security"=hex:01,00,04,80,14,00,00,00,20,00,00,00,00,00,00,00,2c,00,00,00,01,\
01,00,00,00,00,00,05,12,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,02,00,\
7c,00,05,00,00,00,00,00,14,00,bf,03,0f,00,01,01,00,00,00,00,00,05,12,00,00,\
00,00,00,14,00,89,00,0f,00,01,01,00,00,00,00,00,05,13,00,00,00,00,00,14,00,\
81,00,00,00,01,01,00,00,00,00,00,05,14,00,00,00,00,00,24,00,bf,03,0f,00,01,\
05,00,00,00,00,00,05,15,00,00,00,8a,9c,78,08,c9,97,46,5c,a8,cc,7d,42,b4,07,\
00,00,00,00,14,00,01,00,00,00,01,01,00,00,00,00,00,05,04,00,00,00
My first attempt was to create these ACLs programmatically using PowerShell's security descriptor classes. This seemed straightforward:
# My initial (flawed) approach
$SecurityDescriptor = New-Object System.Security.AccessControl.RawSecurityDescriptor
([System.Security.AccessControl.ControlFlags]::DiscretionaryAclPresent, $null, $null,
$null, $null)
$ACE = New-Object System.Security.AccessControl.CommonAce(
[System.Security.AccessControl.AceFlags]::None,
[System.Security.AccessControl.AceQualifier]::AccessAllowed,
983999, # Access mask
$UserSID,
$false,
$null
)
However, I discovered that PowerShell-created ACLs produce slightly different binary structures than GUI-created ones by trial and error, even when the logical permissions appear identical. Users could be added successfully, but RDP authentication would fail with "access denied" errors.
Solution : Binary SID Replacement
After extensive troubleshooting, I found the solution: instead of creating new ACLs from scratch, I perform surgical SID replacement within existing working binary data.
Here's my working registry export from a functioning server:
# Working binary ACL data (hex format)
$WorkingHexData = "01,00,04,80,14,00,00,00,20,00,00,00,00,00,00,00,2c,00,00,00,01,01,00,00,
00,00,00,05,12,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,02,00,7c,00,05,00,00,00,00,00,
14,00,bf,03,0f,00,01,01,00,00,00,00,00,05,12,00,00,00,00,00,14,00,89,00,0f,00,01,01,00,00,
00,00,00,05,13,00,00,00,00,00,14,00,81,00,00,00,01,01,00,00,00,00,00,05,14,00,00,00,00,00,
24,00,bf,03,0f,00,01,05,00,00,00,00,00,05,15,00,00,00,8a,9c,78,08,c9,97,46,5c,a8,cc,7d,42,
32,05,00,00,00,00,14,00,01,00,00,00,01,01,00,00,00,00,00,05,04,00,00,00"
This binary data represents an ACL with the following structure:
- NT AUTHORITY\SYSTEM - Access: 983999 (Full Control)
- NT AUTHORITY\LOCAL SERVICE - Access: 983177
- NT AUTHORITY\NETWORK SERVICE - Access: 129
- Various BEAR\Domain Groups - Access: 983999
- NT AUTHORITY\INTERACTIVE - Access: 1 (Guest Access)
Instead of creating new ACLs, I replace existing SIDs within the working binary structure:
function Replace-SIDInBinary {
param($OldUserName, $OldDomain, $NewUserName, $NewDomain)
$RegPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
# Get SIDs for old and new accounts
$OldAccount = New-Object System.Security.Principal.NTAccount($OldDomain, $OldUserName)
$OldSID = $OldAccount.Translate([System.Security.Principal.SecurityIdentifier])
$NewAccount = New-Object System.Security.Principal.NTAccount($NewDomain, $NewUserName)
$NewSID = $NewAccount.Translate([System.Security.Principal.SecurityIdentifier])
# Load current binary ACL
$SecurityProperty = Get-ItemProperty -Path $RegPath -Name "Security"
$SecurityDescriptor = New-Object System.Security.AccessControl.RawSecurityDescriptor
($SecurityProperty.Security, 0)
# Find the target ACE to replace
for ($i = 0; $i -lt $SecurityDescriptor.DiscretionaryAcl.Count; $i++) {
$ACE = $SecurityDescriptor.DiscretionaryAcl[$i]
if ($ACE.SecurityIdentifier.Equals($OldSID)) {
# Found target - preserve all properties except SID
$accessMask = $ACE.AccessMask
# Remove old ACE
$SecurityDescriptor.DiscretionaryAcl.RemoveAce($i)
# Create replacement with identical properties
$NewACE = New-Object System.Security.AccessControl.CommonAce(
$ACE.AceFlags,
$ACE.AceQualifier,
$accessMask,
$NewSID,
$ACE.IsCallback,
$ACE.GetOpaque()
)
# Insert at same position
$SecurityDescriptor.DiscretionaryAcl.InsertAce($i, $NewACE)
break
}
}
# Convert back to binary and apply
$NewSecurityBytes = New-Object byte[] $SecurityDescriptor.BinaryLength
$SecurityDescriptor.GetBinaryForm($NewSecurityBytes, 0)
Stop-Service -Name TermService -Force
Set-ItemProperty -Path $RegPath -Name "Security" -Value $NewSecurityBytes
Start-Service -Name TermService
}
Key Insights/Lessons Learned
1. Access Mask Values Matter
The GUI uses specific access mask values that differ from standard Windows permissions:
- 983999: Full Control for Terminal Services
- 983177: LOCAL SERVICE access level
- 129: NETWORK SERVICE access level
- 1: Guest/Interactive access
Binary Structure Preservation
The critical discovery was that the exact binary structure created by the GUI must be preserved. PowerShell's security descriptor classes, while logically equivalent, produce subtly different binary representations that cause authentication failures.
Service Dependencies
Terminal Services must be stopped and restarted when modifying the Security registry value:
Stop-Service -Name TermService -Force
# Modify registry
Start-Service -Name TermService
Position Matters
The order of ACEs in the ACL affects functionality. The replacement method preserves the exact position of each entry, maintaining the working structure if this is broken the key is marked as invalid.
Working with Binary Data
This is how the script combines working binary data import with interactive SID replacement:
function Import-WorkingBinaryData {
$HexData = "01,00,04,80,14,00,00,00..." # Full hex string
$ByteArray = $HexData.Split(',') | ForEach-Object { [Convert]::ToByte($_.Trim(), 16) }
$RegPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
Stop-Service -Name TermService -Force
Set-ItemProperty -Path $RegPath -Name "Security" -Value $ByteArray
Start-Service -Name TermService
}
function Interactive-SIDReplacement {
# Display current ACL with numbered options
# Allow user to select which account to replace
# Perform SID replacement using proven method
}
Usage
This is how you add a new user to the ACL, when run you will be asked with ACL you would like to replace on the ACL.
.\rdp-permissions-manager.ps1 -NewUserName "bear.user"
When this command is run, remote desktop will be reset and on reconnection you will be presented with a list of users that can be replaced, here you can see we only have one "slot" left to replace which is option 1:
Can we decode the hexadecimal this string?
This is the string I have as an example but can this be decoded, well yes to a degree this is how the value is stored in the registry as a string which is then in turn hex data as below:
hex:01,00,04,80,14,00,00,00,20,00,00,00,00,00,00,00,2c,00,00,00,01,\
01,00,00,00,00,00,05,12,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,02,00,\
7c,00,05,00,00,00,00,00,14,00,bf,03,0f,00,01,01,00,00,00,00,00,05,12,00,00,\
00,00,00,14,00,89,00,0f,00,01,01,00,00,00,00,00,05,13,00,00,00,00,00,14,00,\
81,00,00,00,01,01,00,00,00,00,00,05,14,00,00,00,00,00,24,00,bf,03,0f,00,01,\
05,00,00,00,00,00,05,15,00,00,00,8a,9c,78,08,c9,97,46,5c,a8,cc,7d,42,b4,07,\
00,00,00,00,14,00,01,00,00,00,01,01,00,00,00,00,00,05,04,00,00,00
Lets take that Hex data and use the script below to visualise it as it is broken down:
Script : Decode-Hex.ps1
# Visual hex breakdown showing exactly how SIDs are extracted
function Show-VisualHexBreakdown {
# Your exact hex data
$HexData = "01,00,04,80,14,00,00,00,20,00,00,00,00,00,00,00,2c,00,00,00,01,01,00,00,00,
00,00,05,12,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,02,00,7c,00,05,00,00,00,00,00,
14,00,bf,03,0f,00,01,01,00,00,00,00,00,05,12,00,00,00,00,00,14,00,89,00,0f,00,01,01,00,
00,00,00,00,05,13,00,00,00,00,00,14,00,81,00,00,00,01,01,00,00,00,00,00,05,14,00,00,00,
00,00,24,00,bf,03,0f,00,01,05,00,00,00,00,00,05,15,00,00,00,8a,9c,78,08,c9,97,46,5c,a8,
cc,7d,42,b4,07,00,00,00,00,14,00,01,00,00,00,01,01,00,00,00,00,00,05,04,00,00,00"
$ByteArray = $HexData.Split(',') | ForEach-Object { [Convert]::ToByte($_.Trim(), 16) }
Write-Host "=== VISUAL HEX TO SID BREAKDOWN ===" -ForegroundColor Cyan
Write-Host ""
# SID locations found by analysis
$SIDLocations = @(
@{ Offset = 20; Name = "NT AUTHORITY\SYSTEM"; SID = "S-1-5-18" },
@{ Offset = 80; Name = "NT AUTHORITY\LOCAL SERVICE"; SID = "S-1-5-19" },
@{ Offset = 100; Name = "NT AUTHORITY\NETWORK SERVICE"; SID = "S-1-5-20" },
@{ Offset = 120; Name = "BEAR\lee.croucher"; SID = "S-1-5-21-142122122-1548130249-
1115540648-1972" },
@{ Offset = 156; Name = "NT AUTHORITY\INTERACTIVE"; SID = "S-1-5-4" }
)
foreach ($Location in $SIDLocations) {
$Offset = $Location.Offset
$Name = $Location.Name
$ExpectedSID = $Location.SID
Write-Host "## SID: $Name" -ForegroundColor Green
Write-Host "Location: Byte $Offset" -ForegroundColor Yellow
# Extract SID components
$Revision = $ByteArray[$Offset]
$SubAuthCount = $ByteArray[$Offset + 1]
$Authority = $ByteArray[$Offset + 7]
# Calculate SID length
$SIDLength = 8 + ($SubAuthCount * 4)
# Extract hex bytes for this SID
$SIDBytes = $ByteArray[$Offset..($Offset + $SIDLength - 1)]
$SIDHex = ($SIDBytes | ForEach-Object { $_.ToString('X2') }) -join ' '
Write-Host "Hex: $SIDHex" -ForegroundColor White
# Visual breakdown with arrows
if ($SubAuthCount -eq 1) {
# Short SID (well-known)
$SubAuth1Bytes = $ByteArray[($Offset + 8)..($Offset + 11)]
$SubAuth1 = [BitConverter]::ToUInt32($SubAuth1Bytes, 0)
Write-Host "│ │ │ │ │" -ForegroundColor DarkGray
Write-Host "│ │ │ │ └─ SubAuth 1: $SubAuth1 (0x$($ByteArray[$Offset + 8].ToString('X2')) $($ByteArray[$Offset + 9].ToString('X2')) $($ByteArray[$Offset + 10].ToString('X2')) $($ByteArray[$Offset + 11].ToString('X2')))" -ForegroundColor White
Write-Host "│ │ │ └─ Authority: $Authority" -ForegroundColor White
Write-Host "│ │ └─ SubAuth Count: $SubAuthCount" -ForegroundColor White
Write-Host "│ └─ Revision: $Revision" -ForegroundColor White
} else {
# Long SID (domain)
Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor DarkGray
# Extract all sub-authorities
for ($i = $SubAuthCount - 1; $i -ge 0; $i--) {
$SubAuthOffset = $Offset + 8 + ($i * 4)
$SubAuthBytes = $ByteArray[$SubAuthOffset..($SubAuthOffset + 3)]
$SubAuthValue = [BitConverter]::ToUInt32($SubAuthBytes, 0)
$SubAuthHex = ($SubAuthBytes | ForEach-Object { $_.ToString('X2') }) -join ' '
if ($i -eq $SubAuthCount - 1) {
Write-Host "│ │ │ │ │ │ │ │ └─ RID: $SubAuthValue (0x$SubAuthHex)" -ForegroundColor White
} elseif ($i -eq 0) {
Write-Host "│ │ │ │ └─ SubAuth $($i + 1): $SubAuthValue (domain indicator)" -ForegroundColor White
} else {
Write-Host "│ │ │ │ │ │ └─ Domain Part $($SubAuthCount - $i - 1): $SubAuthValue" -ForegroundColor White
}
}
Write-Host "│ │ │ └─ Authority: $Authority" -ForegroundColor White
Write-Host "│ │ └─ SubAuth Count: $SubAuthCount" -ForegroundColor White
Write-Host "│ └─ Revision: $Revision" -ForegroundColor White
}
Write-Host "Result: $ExpectedSID = $Name" -ForegroundColor Green
Write-Host ""
}
Write-Host "=== BREAKDOWN EXPLANATION ===" -ForegroundColor Yellow
Write-Host "• Each SID starts with revision byte (01)" -ForegroundColor White
Write-Host "• Sub-authority count determines SID length" -ForegroundColor White
Write-Host "• Authority is always 5 for NT Authority" -ForegroundColor White
Write-Host "• Sub-authorities are 4-byte little-endian values" -ForegroundColor White
Write-Host "• Well-known SIDs have 1 sub-authority" -ForegroundColor White
Write-Host "• Domain SIDs have 5 sub-authorities (21 + 3 domain parts + RID)" -ForegroundColor White
}
# Run the visual breakdown
Show-VisualHexBreakdown
05,00,00,00,00,00,05,15,00,00,00,8a,9c,78,08,c9,97,46,5c,a8,cc,7d,42,b4,07,\
This will then give you the breakdown of the permissions as they are applied for example the "system" account which is this one broken down looks like this:
Hex: 01 01 00 00 00 00 00 05 12 00 00 00
│ │ │ │ │
│ │ │ │ └─ SubAuth: 18 (0x12)
│ │ │ └─ Authority: 5
│ │ └─ SubAuth Count: 1
│ └─ Revision: 1
└─ Result: S-1-5-18
Then if we look at the domain based breakdown that follows this outline, this translates to my account:
Hex: 01 05 00 00 00 00 00 05 15 00 00 00 8a 9c 78 08 c9 97 46 5c a8 cc 7d 42 b4 07 00 00
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ RID: 1972
│ │ │ │ │ └─ Domain ID part 3
│ │ │ │ └─ Domain ID part 2
│ │ │ └─ Domain ID part 1
│ │ └─ SubAuth: 21 (domain)
│ └─ SubAuth Count: 5
└─ Result: S-1-5-21-142122122-1548130249-1115540648-1972
If you would rather see the breakdown without all the graphics and you require a summary of the permissions like this:
SECURITY DESCRIPTOR PROPERTIES:
Control Flags: DiscretionaryAclPresent, SelfRelative
Revision:
Binary Length: 168 bytes
DISCRETIONARY ACCESS CONTROL LIST (DACL):
DACL Revision: 2
Total ACEs: 5
DACL Binary Length: 124 bytes
=== ACCESS CONTROL ENTRIES (ACEs) ===
ACE #1:
─────────────
Account: NT AUTHORITY\SYSTEM
SID: S-1-5-18
Access Mask: 983999 (0x000F03BF)
Description: Full Control (Terminal Services)
ACE Type: AccessAllowed
ACE Flags: None
Is Inherited: False
Binary Length: 20 bytes
Access Rights Breakdown:
• Delete/Write DAC/Write Owner
• Read Control
• Synchronize
• Logon
• Logoff
• Message
• Disconnect
• Virtual Channels
• Remote Control
• Reset
• Shadow
• Query
ACE #2:
─────────────
Account: NT AUTHORITY\LOCAL SERVICE
SID: S-1-5-19
Access Mask: 983177 (0x000F0089)
Description: LOCAL SERVICE Access
ACE Type: AccessAllowed
ACE Flags: None
Is Inherited: False
Binary Length: 20 bytes
Access Rights Breakdown:
• Delete/Write DAC/Write Owner
• Read Control
• Synchronize
• Message
• Remote Control
• Query
Then you can simply use this script to get more human readable permissions:
Script : SecurityKey-Decrypter.ps1
# Decode the Security registry key from your .reg file
function Decode-RDPTcpSecurity {
# Your exact hex data from the .reg file
$HexData = "01,00,04,80,14,00,00,00,20,00,00,00,00,00,00,00,2c,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,02,00,7c,00,05,00,00,00,00,00,14,00,bf,03,0f,00,01,01,00,00,00,00,00,05,12,00,00,00,00,00,14,00,89,00,0f,00,01,01,00,00,00,00,00,05,13,00,00,00,00,00,14,00,81,00,00,00,01,01,00,00,00,00,00,05,14,00,00,00,00,00,24,00,bf,03,0f,00,01,05,00,00,00,00,00,05,15,00,00,00,8a,9c,78,08,c9,97,46,5c,a8,cc,7d,42,b4,07,00,00,00,00,14,00,01,00,00,00,01,01,00,00,00,00,00,05,04,00,00,00"
# Convert hex string to bytes
$ByteArray = $HexData.Split(',') | ForEach-Object { [Convert]::ToByte($_.Trim(), 16) }
Write-Host "=== RDP-TCP SECURITY KEY BREAKDOWN ===" -ForegroundColor Cyan
Write-Host "Source: Live-Mine-All-Mine.reg" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Cyan
try {
# Create security descriptor from binary data
$SecurityDescriptor = New-Object System.Security.AccessControl.RawSecurityDescriptor($ByteArray, 0)
Write-Host "`nSECURITY DESCRIPTOR PROPERTIES:" -ForegroundColor Green
Write-Host "Control Flags: $($SecurityDescriptor.ControlFlags)" -ForegroundColor White
Write-Host "Revision: $($SecurityDescriptor.Revision)" -ForegroundColor White
Write-Host "Binary Length: $($SecurityDescriptor.BinaryLength) bytes" -ForegroundColor White
if ($SecurityDescriptor.DiscretionaryAcl) {
Write-Host "`nDISCRETIONARY ACCESS CONTROL LIST (DACL):" -ForegroundColor Green
Write-Host "DACL Revision: $($SecurityDescriptor.DiscretionaryAcl.Revision)" -ForegroundColor White
Write-Host "Total ACEs: $($SecurityDescriptor.DiscretionaryAcl.Count)" -ForegroundColor White
Write-Host "DACL Binary Length: $($SecurityDescriptor.DiscretionaryAcl.BinaryLength) bytes" -ForegroundColor White
Write-Host "`n=== ACCESS CONTROL ENTRIES (ACEs) ===" -ForegroundColor Yellow
for ($i = 0; $i -lt $SecurityDescriptor.DiscretionaryAcl.Count; $i++) {
$ACE = $SecurityDescriptor.DiscretionaryAcl[$i]
Write-Host "`nACE #$($i + 1):" -ForegroundColor Magenta
Write-Host "─────────────" -ForegroundColor Gray
# Resolve account name
try {
$AccountName = $ACE.SecurityIdentifier.Translate([System.Security.Principal.NTAccount]).Value
Write-Host "Account: $AccountName" -ForegroundColor Green
} catch {
Write-Host "Account: [Could not resolve]" -ForegroundColor Red
}
Write-Host "SID: $($ACE.SecurityIdentifier)" -ForegroundColor Gray
# Decode access mask
$AccessMask = $ACE.AccessMask
$AccessDescription = switch ($AccessMask) {
983999 { "Full Control (Terminal Services)" }
983177 { "LOCAL SERVICE Access" }
129 { "NETWORK SERVICE Access" }
1 { "Guest/Interactive Access" }
default { "Custom Access" }
}
Write-Host "Access Mask: $AccessMask (0x$($AccessMask.ToString('X8')))" -ForegroundColor Cyan
Write-Host "Description: $AccessDescription" -ForegroundColor Cyan
# ACE details
Write-Host "ACE Type: $($ACE.AceType)" -ForegroundColor White
Write-Host "ACE Flags: $($ACE.AceFlags)" -ForegroundColor White
Write-Host "Is Inherited: $($ACE.IsInherited)" -ForegroundColor White
Write-Host "Binary Length: $($ACE.BinaryLength) bytes" -ForegroundColor White
# Break down the access mask into component rights
Write-Host "Access Rights Breakdown:" -ForegroundColor Yellow
$Rights = @()
# Terminal Services specific rights (educated guesses based on common values)
if ($AccessMask -band 0x000F0000) { $Rights += "Delete/Write DAC/Write Owner" }
if ($AccessMask -band 0x00020000) { $Rights += "Read Control" }
if ($AccessMask -band 0x00010000) { $Rights += "Synchronize" }
if ($AccessMask -band 0x00000800) { $Rights += "Query Information" }
if ($AccessMask -band 0x00000400) { $Rights += "Set Information" }
if ($AccessMask -band 0x00000200) { $Rights += "Logon" }
if ($AccessMask -band 0x00000100) { $Rights += "Logoff" }
if ($AccessMask -band 0x00000080) { $Rights += "Message" }
if ($AccessMask -band 0x00000040) { $Rights += "Connect" }
if ($AccessMask -band 0x00000020) { $Rights += "Disconnect" }
if ($AccessMask -band 0x00000010) { $Rights += "Virtual Channels" }
if ($AccessMask -band 0x00000008) { $Rights += "Remote Control" }
if ($AccessMask -band 0x00000004) { $Rights += "Reset" }
if ($AccessMask -band 0x00000002) { $Rights += "Shadow" }
if ($AccessMask -band 0x00000001) { $Rights += "Query" }
if ($Rights.Count -gt 0) {
$Rights | ForEach-Object { Write-Host " • $_" -ForegroundColor DarkGray }
} else {
Write-Host " • No specific rights identified" -ForegroundColor DarkGray
}
}
Write-Host "`n=== SUMMARY ===" -ForegroundColor Green
Write-Host "This ACL contains $($SecurityDescriptor.DiscretionaryAcl.Count) access control entries:" -ForegroundColor White
for ($i = 0; $i -lt $SecurityDescriptor.DiscretionaryAcl.Count; $i++) {
$ACE = $SecurityDescriptor.DiscretionaryAcl[$i]
try {
$AccountName = $ACE.SecurityIdentifier.Translate([System.Security.Principal.NTAccount]).Value
} catch {
$AccountName = $ACE.SecurityIdentifier.Value
}
$AccessDescription = switch ($ACE.AccessMask) {
983999 { "Full Control" }
983177 { "LOCAL SERVICE" }
129 { "NETWORK SERVICE" }
1 { "Guest Access" }
default { "Custom ($($ACE.AccessMask))" }
}
Write-Host " [$($i + 1)] $AccountName - $AccessDescription" -ForegroundColor White
}
Write-Host "`n=== INTERPRETATION ===" -ForegroundColor Yellow
Write-Host "• This is a CUSTOM RDP-Tcp ACL that bypasses Remote Desktop Users group" -ForegroundColor White
Write-Host "• Only the accounts listed above can RDP to this server" -ForegroundColor White
Write-Host "• Access mask 983999 = Full Terminal Services control" -ForegroundColor White
Write-Host "• Access mask 1 = Basic query/interactive rights" -ForegroundColor White
Write-Host "• The ACL includes system accounts (SYSTEM, LOCAL/NETWORK SERVICE)" -ForegroundColor White
Write-Host "• One domain account appears to have full RDP access" -ForegroundColor White
} else {
Write-Host "No Discretionary ACL found" -ForegroundColor Red
}
} catch {
Write-Error "Error decoding security descriptor: $($_.Exception.Message)"
}
Write-Host "`n=== HEX DUMP (First 100 bytes) ===" -ForegroundColor Gray
$HexDump = ""
for ($i = 0; $i -lt [Math]::Min(100, $ByteArray.Length); $i++) {
$HexDump += $ByteArray[$i].ToString("X2") + " "
if (($i + 1) % 16 -eq 0) { $HexDump += "`n" }
}
Write-Host $HexDump -ForegroundColor DarkGray
}
# Run the decoder
Decode-RDPTcpSecurity
Conclusion
Managing RDP-Tcp custom permissions requires understanding the intricate relationship between GUI-created binary structures and programmatic manipulation. The key insight is that preservation of the original binary format is crucial - replacement works where creation fails.
This technique provides a powerful alternative to traditional group-based RDP management, especially useful in environments requiring strict access control or when bypassing group policies is necessary and due to its legacy requirements it is quite hard to detect and diagnose.