Last active
July 28, 2025 07:48
-
-
Save mayerwin/153839aa5184cbf7a3fd3b07423c4f0c to your computer and use it in GitHub Desktop.
Headless RDP auto‑logon at startup (Windows Server 2022+). Creates a hidden FreeRDP connection at boot (to fire user logon triggers), then kills it after 60 s. Stores creds securely in Credential Manager.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Installs (or uninstalls) a headless RDP‐at‐startup scheduled task and optional per‑user logon tasks. | |
| .DESCRIPTION | |
| When run without -Disable, this script: | |
| • Ensures it’s elevated to Administrator. | |
| • Stores (or reuses) RDP credentials in Windows Credential Manager. | |
| • Downloads the FreeRDP client (if not already present) to .\bin\sdl3‑freerdp.exe. | |
| • Registers a “HeadlessRdp_Bootstrap” task under \ScheduleAutoLogon\ that, at system startup, | |
| spawns a background RDP session to $Host:$Port, waits, then tears it down—triggering | |
| any per‑user AtLogOn tasks. | |
| When run with -Disable, it cleans up: removes the scheduled tasks, deletes the credential entry, | |
| and deletes the .\bin folder. | |
| .PARAMETER Disable | |
| Switch: if present, uninstalls the scheduled tasks, removes credentials and binaries, then exits. | |
| .PARAMETER RunAsUser | |
| The user name under which the task(s) will run. Defaults to $env:USERNAME. | |
| .PARAMETER RunAsDomain | |
| The domain (or machine name) for the RunAsUser. Defaults to $env:USERDOMAIN. | |
| .NOTES | |
| • Tested on Windows Server 2022 with PowerShell 5.1. | |
| • Credential retrieval uses Win32 CredRead via P/Invoke. | |
| • To tweak the RDP target, adjust the $Host and $Port constants below. | |
| .EXAMPLE | |
| # Install for the currently logged in user on the local machine | |
| .\ScheduleAutoLogon.ps1 | |
| .EXAMPLE | |
| # Uninstall everything | |
| .\ScheduleAutoLogon.ps1 -Disable | |
| #> | |
| param( | |
| [switch]$Disable, | |
| [string]$RunAsUser = $env:USERNAME, | |
| [string]$RunAsDomain = $env:USERDOMAIN | |
| ) | |
| $ErrorActionPreference = 'Stop' | |
| trap { | |
| Write-Host ""; Write-Host ('FAILED: {0}' -f $_.Exception.Message) -ForegroundColor Red | |
| Read-Host 'Press Enter to close'; exit 1 | |
| } | |
| # ── Self‑elevate ────────────────────────────────────────────────────────────── | |
| $cur = [Security.Principal.WindowsIdentity]::GetCurrent() | |
| if (-not (New-Object Security.Principal.WindowsPrincipal $cur).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { | |
| $a = @('-NoProfile','-ExecutionPolicy','Bypass','-File',$PSCommandPath) | |
| if ($Disable) { $a += '-Disable' } | |
| $a += '-RunAsUser'; $a += $RunAsUser | |
| $a += '-RunAsDomain'; $a += $RunAsDomain | |
| Start-Process -FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -Verb RunAs -ArgumentList $a | |
| exit | |
| } | |
| [Net.ServicePointManager]::SecurityProtocol = 3072 # TLS 1.2 | |
| Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force | |
| # ── Constants ───────────────────────────────────────────────────────────────── | |
| $taskUser = "$RunAsDomain\$RunAsUser" | |
| $tsHost = "127.0.0.1" | |
| $tsPort = 3089 | |
| $rootDir = Split-Path -Parent $PSCommandPath | |
| $binDir = Join-Path $rootDir 'bin' | |
| $freerdp = Join-Path $binDir 'sdl3-freerdp.exe' | |
| $taskPath = '\ScheduleAutoLogon\' | |
| $rdpTask = 'HeadlessRdp_Bootstrap' | |
| $exeUrl = 'https://ci.freerdp.com/job/freerdp-nightly-windows/arch=win64,label=vs2017/lastSuccessfulBuild/artifact/install/bin/sdl3-freerdp.exe' | |
| $credTag = 'TERMSRV/' + $tsHost + ':' + $tsPort | |
| $logFile = "$binDir\freerdp.log" | |
| # ── Uninstall path ─────────────────────────────────────────────────────────── | |
| function Uninstall-RdpStartup { | |
| Write-Host 'Removing tasks and credentials ...' -ForegroundColor Yellow | |
| Get-ScheduledTask -TaskPath $taskPath -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false | |
| schtasks /Delete /TN $rdpTask /F 2>$null | Out-Null | |
| cmdkey /delete:$credTag 2>$null | Out-Null | |
| if (Test-Path $binDir) { Remove-Item $binDir -Recurse -Force } | |
| Write-Host 'Clean removal complete.' -ForegroundColor Green | |
| Read-Host 'Press Enter to exit'; exit | |
| } | |
| if ($Disable) { Uninstall-RdpStartup } | |
| # ── Ensure credential exists ───────────────────────────────────────────────── | |
| if (-not (cmdkey /list | Select-String $credTag)) { | |
| $pwd = Read-Host "Password for $taskUser (stored in Credential Manager)" -AsSecureString | |
| $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([ | |
| Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd)) | |
| cmdkey /generic:$credTag /user:$taskUser /pass:$plain | Out-Null | |
| } | |
| function Get-StoredGenericCredential { | |
| param([string]$TargetName) | |
| Add-Type -TypeDefinition @" | |
| using System; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| public class NativeCred { | |
| [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)] | |
| public static extern bool CredRead( | |
| string target, int type, int reservedFlag, out IntPtr credentialPtr); | |
| [DllImport("advapi32.dll", SetLastError=true)] | |
| public static extern bool CredFree(IntPtr buffer); | |
| [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] | |
| public struct CREDENTIAL { | |
| public UInt32 Flags; | |
| public UInt32 Type; | |
| public IntPtr TargetName; | |
| public IntPtr Comment; | |
| public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; | |
| public UInt32 CredentialBlobSize; | |
| public IntPtr CredentialBlob; | |
| public UInt32 Persist; | |
| public UInt32 AttributeCount; | |
| public IntPtr Attributes; | |
| public IntPtr TargetAlias; | |
| public IntPtr UserName; | |
| } | |
| } | |
| "@ | |
| $ptr = [IntPtr]::Zero | |
| if (-not [NativeCred]::CredRead($TargetName, 1, 0, [ref]$ptr)) { | |
| throw "CredRead failed: $([Runtime.InteropServices.Marshal]::GetLastWin32Error())" | |
| } | |
| $cred = [Runtime.InteropServices.Marshal]::PtrToStructure( | |
| $ptr, [Type][NativeCred+CREDENTIAL]) | |
| $pwdBytes = New-Object byte[] $cred.CredentialBlobSize | |
| [Runtime.InteropServices.Marshal]::Copy( | |
| $cred.CredentialBlob, $pwdBytes, 0, $cred.CredentialBlobSize) | |
| # Suppress the CredFree boolean: | |
| [void][NativeCred]::CredFree($ptr) | |
| # or: [NativeCred]::CredFree($ptr) | Out-Null | |
| # blob is UTF‑16 little‑endian | |
| $plain = [Text.Encoding]::Unicode.GetString($pwdBytes).TrimEnd([char]0) | |
| return $plain | |
| } | |
| $plain = Get-StoredGenericCredential -TargetName $credTag | |
| # ── Download FreeRDP if missing ────────────────────────────────────────────── | |
| if (-not (Test-Path $freerdp)) { | |
| Write-Host 'Downloading sdl3-freerdp.exe ...' -ForegroundColor Cyan | |
| if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir | Out-Null } | |
| try { Invoke-WebRequest -Uri $exeUrl -OutFile $freerdp -UseBasicParsing } | |
| catch { (New-Object System.Net.WebClient).DownloadFile($exeUrl,$freerdp) } | |
| } | |
| # ── Build FreeRDP argument list ────────────────────────────────────────────── | |
| $rdpArgs = @( | |
| '/v:' + $tsHost + ':' + $tsPort, | |
| "/u:$RunAsUser", | |
| "/d:$RunAsDomain", | |
| "/p:$plain", | |
| '/size:1280x800', | |
| # '/log-level:TRACE', | |
| '/cert:ignore', | |
| '/sec:nla' | |
| ) -join ' ' | |
| # ── Build cmd.exe wrapper with WLog appender vars ──────────────────────────── | |
| # Wrap it with START, TIMEOUT, and TASKKILL | |
| $cmd = '/c ' | |
| $cmd += 'set "WLOG_APPENDER=file" && ' | |
| $cmd += 'set "WLOG_FILEAPPENDER_TRUNCATE=TRUE" && ' | |
| $cmd += 'set "WLOG_FILEAPPENDER_OUTPUT_FILE_PATH=' + $binDir + '" && ' | |
| $cmd += 'set "WLOG_FILEAPPENDER_OUTPUT_FILE_NAME=freerdp.log" && ' | |
| # 1) Start in background (no /wait) so cmd.exe returns immediately | |
| $cmd += 'start "" /B "' + $freerdp + '" ' + $rdpArgs + ' && ' | |
| # 2) Wait 60 seconds for the session to log on and your tasks to fire | |
| $cmd += 'timeout /T 60 /NOBREAK >nul && ' | |
| # 3) Kill the client by image name | |
| $cmd += 'taskkill /IM ' + [IO.Path]::GetFileName($freerdp) + ' /F' | |
| # ── Remove existing tasks ──────────────────────────────────────────────────── | |
| Get-ScheduledTask -TaskPath $taskPath -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue | |
| # ── Register headless task ─────────────────────────────────────────────────── | |
| $action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument $cmd -WorkingDirectory $binDir | |
| $trigger = New-ScheduledTaskTrigger -AtStartup | |
| $principal = New-ScheduledTaskPrincipal -UserId $taskUser -LogonType S4U -RunLevel Highest | |
| $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::FromMinutes(3)) | |
| Register-ScheduledTask -TaskName $rdpTask -TaskPath $taskPath -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | |
| # ── Finished ──────────────────────────────────────────────────────────────── | |
| Write-Host '' | |
| Write-Host 'SUCCESS - hidden RDP session will spawn at boot; console remains locked.' -ForegroundColor Green | |
| Read-Host 'Press Enter to exit' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment