PrepareOOBE.ps1
|
<#PSScriptInfo .VERSION 1.7 .GUID b28d9ccb-b2e5-4c78-8270-2a53c80ab0f6 .AUTHOR admin.descampd .COMPANYNAME .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .DESCRIPTION 1.7 #> Param() # Version : 1.5 # Changelog: # 1.0 - Initial release # 1.1 - Added force-removal of locked CCM directories via robocopy # 1.2 - Added MDM/co-management enrollment cleanup for Autopilot ZTID # 1.3 - Added CloudDomainJoin and OOBE state purge to prevent co-management re-enrollment # 1.4 - Verbose logging on every step, dsregcmd status capture, cleanup summary # 1.5 - Added NGC, Cloud AP token cache, Autopilot provisioning cache, AAD cert wipe for ZTID $ScriptBlock = @' $LogPath = "C:\Windows\Temp\PrepareOOBE.log" $Script:Removed = 0 $Script:NotFound = 0 $Script:Failed = 0 function Write-Log { param( [string]$Message, [string]$Level = "Info" ) $Stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Add-Content -Path $LogPath -Value "[$Stamp] [$Level] $Message" -ErrorAction SilentlyContinue $Color = switch ($Level) { "Warning" { "Yellow" } "Error" { "Red" } "Success" { "Green" } default { if ($Message -match '^\s*\[REMOVED\]') { "Green" } elseif ($Message -match '^\s*\[FAILED\]') { "Red" } elseif ($Message -match '^\s*\[KILLED\]') { "Magenta" } elseif ($Message -match '^\s*\[NOT FOUND\]'){ "DarkGray" } elseif ($Message -match '^\s*\[FOUND\]') { "Cyan" } elseif ($Message -match '^\s*\[WARNING\]') { "Yellow" } else { "White" } } } Write-Host "[$Stamp] [$Level] $Message" -ForegroundColor $Color } function Write-Step { param([string]$Title) $Sep = "-" * 60 Write-Log "" Write-Host "" Write-Host $Sep -ForegroundColor DarkCyan Write-Host " $Title" -ForegroundColor Cyan Write-Host $Sep -ForegroundColor DarkCyan Add-Content -Path $LogPath -Value $Sep -ErrorAction SilentlyContinue Add-Content -Path $LogPath -Value " $Title" -ErrorAction SilentlyContinue Add-Content -Path $LogPath -Value $Sep -ErrorAction SilentlyContinue } function Remove-RegKey { param([string]$Path) if (Test-Path $Path) { $children = (Get-ChildItem $Path -Recurse -ErrorAction SilentlyContinue).Count Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path $Path)) { Write-Log " [REMOVED] $Path ($children sub-items)" $Script:Removed++ } else { Write-Log " [FAILED] $Path - still present after removal" -Level Warning $Script:Failed++ } } else { Write-Log " [NOT FOUND] $Path" $Script:NotFound++ } } function Remove-LockedDirectory { param([string]$Path) if (-not (Test-Path $Path)) { Write-Log " [NOT FOUND] $Path" $Script:NotFound++ return } $FileCount = (Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue).Count Write-Log " [FOUND] $Path ($FileCount files/folders) - starting robocopy wipe" $Empty = "$env:TEMP\__empty_robocopy" New-Item -ItemType Directory -Path $Empty -Force -ErrorAction SilentlyContinue | Out-Null $RoboResult = Start-Process "robocopy.exe" ` -ArgumentList "`"$Empty`" `"$Path`" /MIR /NFL /NDL /NJH /NJS /R:1 /W:1" ` -Wait -NoNewWindow -PassThru -ErrorAction SilentlyContinue Write-Log " [ROBOCOPY] Exit code: $($RoboResult.ExitCode) (<=7 = success)" Remove-Item $Empty -Force -Recurse -ErrorAction SilentlyContinue Start-Process "takeown.exe" -ArgumentList "/f `"$Path`" /r /d y" -Wait -NoNewWindow -ErrorAction SilentlyContinue | Out-Null Start-Process "icacls.exe" -ArgumentList "`"$Path`" /grant administrators:F /t" -Wait -NoNewWindow -ErrorAction SilentlyContinue | Out-Null Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue if (Test-Path $Path) { Write-Log " [FAILED] $Path still present after forced removal" -Level Warning $Script:Failed++ } else { Write-Log " [REMOVED] $Path" $Script:Removed++ } } # ============================================================== Write-Log "================================================================" Write-Log " PrepareOOBE v1.4 started" Write-Log " Computer : $env:COMPUTERNAME" Write-Log " User : $env:USERNAME" Write-Log " Time : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Log "================================================================" # ============================================================== Write-Step "PRE-FLIGHT: dsregcmd /status" # ============================================================== $DsregBefore = & dsregcmd.exe /status 2>&1 $DsregBefore | Where-Object { $_ -match 'AzureAdJoined|WorkplaceJoined|DomainJoined|TenantName|DeviceId|MDMUrl|EnterpriseJoined' } | ForEach-Object { Write-Log " $($_.Trim())" } # ============================================================== Write-Step "STEP 1 - Initial dsregcmd /leave" # ============================================================== Write-Log " Running dsregcmd /leave" $LeaveOut = & dsregcmd.exe /leave 2>&1 $LeaveOut | ForEach-Object { Write-Log " dsregcmd: $($_.ToString().Trim())" } Write-Log " dsregcmd /leave exit code: $LASTEXITCODE" # ============================================================== Write-Step "STEP 2 - Kill CCM processes" # ============================================================== $Processes = @("CcmExec","ccmsetup","CMRcService","CmRcService","smsexec","smsswd","SensorWDService","SensorManagedProvider","ccmrestart","SCNotification") foreach ($ProcName in $Processes) { $procs = Get-Process -Name $ProcName -ErrorAction SilentlyContinue if ($procs) { $ids = $procs.Id -join ", " $procs | Stop-Process -Force -ErrorAction SilentlyContinue Write-Log " [KILLED] $ProcName (PID: $ids)" } else { Write-Log " [NOT FOUND] Process: $ProcName" } } # ============================================================== Write-Step "STEP 3 - Stop and remove CCM services" # ============================================================== foreach ($SvcName in @("CcmExec","ccmsetup","smstsmgr","CmRcService")) { $svc = Get-Service -Name $SvcName -ErrorAction SilentlyContinue if ($svc) { Write-Log " [FOUND] Service $SvcName (Status: $($svc.Status))" Stop-Service -Name $SvcName -Force -ErrorAction SilentlyContinue $ScResult = & sc.exe delete $SvcName 2>&1 Write-Log " [SC DELETE] $SvcName - $ScResult" } else { Write-Log " [NOT FOUND] Service: $SvcName" } } # ============================================================== Write-Step "STEP 4 - Uninstall SCCM client" # ============================================================== if (Test-Path "C:\Windows\ccmsetup\ccmsetup.exe") { Write-Log " [FOUND] ccmsetup.exe - starting /Uninstall" Start-Process -FilePath "C:\Windows\ccmsetup\ccmsetup.exe" -ArgumentList "/Uninstall" -NoNewWindow -ErrorAction SilentlyContinue $TimeoutSec = 300 $Elapsed = 0 $Interval = 5 Write-Log " Polling for uninstall completion (timeout: ${TimeoutSec}s)..." do { Start-Sleep -Seconds $Interval $Elapsed += $Interval $SvcGone = -not (Get-Service -Name "CcmExec" -ErrorAction SilentlyContinue) $ProcGone = -not (Get-Process -Name "ccmsetup" -ErrorAction SilentlyContinue) $ExeGone = -not (Test-Path "C:\Windows\ccmsetup\ccmsetup.exe") Write-Log " [${Elapsed}s] ServiceGone=$SvcGone | ProcessGone=$ProcGone | ExeGone=$ExeGone" } until (($SvcGone -and $ProcGone) -or $Elapsed -ge $TimeoutSec) if ($SvcGone -and $ProcGone) { Write-Log " [DONE] CCM uninstall confirmed after ${Elapsed}s" } else { Write-Log " [WARNING] CCM uninstall timed out after ${TimeoutSec}s - continuing" -Level Warning } } else { Write-Log " [NOT FOUND] ccmsetup.exe - skipping uninstall" } # ============================================================== Write-Step "STEP 5 - Remove CCM WMI namespaces" # ============================================================== foreach ($ns in @("root\ccm","root\ccm\Policy","root\ccm\SoftwareMeteringAgent","root\SmsDm","root\cimv2\SMS")) { try { Get-WmiObject -Namespace $ns -Class "__Namespace" -ErrorAction Stop | Out-Null ([wmiclass]"\\.\$($ns):__Namespace").Delete() | Out-Null Write-Log " [REMOVED] WMI namespace: $ns" $Script:Removed++ } catch { Write-Log " [NOT FOUND] WMI namespace: $ns" $Script:NotFound++ } } # ============================================================== Write-Step "STEP 6 - Remove CCM scheduled tasks" # ============================================================== $CcmTasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskPath -match 'Configuration Manager' -or $_.TaskName -match 'CCM|SMS|SCCM' } if ($CcmTasks) { foreach ($Task in $CcmTasks) { Unregister-ScheduledTask -TaskName $Task.TaskName -TaskPath $Task.TaskPath -Confirm:$false -ErrorAction SilentlyContinue Write-Log " [REMOVED] Task: $($Task.TaskPath)$($Task.TaskName)" $Script:Removed++ } } else { Write-Log " [NOT FOUND] No CCM/SMS scheduled tasks" $Script:NotFound++ } # ============================================================== Write-Step "STEP 7 - Remove SCCM registry keys" # ============================================================== @( "HKLM:\SOFTWARE\Microsoft\CCM", "HKLM:\SOFTWARE\Microsoft\CCMSetup", "HKLM:\SOFTWARE\Microsoft\SMS", "HKLM:\SOFTWARE\Microsoft\SystemCertificates\SMS", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\CCM", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\SMS", "HKLM:\SYSTEM\CurrentControlSet\Services\CcmExec", "HKLM:\SYSTEM\CurrentControlSet\Services\ccmsetup", "HKLM:\SYSTEM\CurrentControlSet\Services\smstsmgr", "HKLM:\SYSTEM\CurrentControlSet\Services\CmRcService" ) | ForEach-Object { Remove-RegKey $_ } # ============================================================== Write-Step "STEP 8 - Remove SCCM directories (robocopy wipe)" # ============================================================== @("C:\Windows\CCM","C:\Windows\CCMCache","C:\Windows\CCMSetup","C:\MININT") | ForEach-Object { Remove-LockedDirectory $_ } if (Test-Path "C:\Windows\SMSCFG.ini") { Remove-Item "C:\Windows\SMSCFG.ini" -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] C:\Windows\SMSCFG.ini" $Script:Removed++ } else { Write-Log " [NOT FOUND] C:\Windows\SMSCFG.ini" $Script:NotFound++ } $MifFiles = Get-ChildItem -Path "C:\Windows" -Filter "sms*.mif" -ErrorAction SilentlyContinue if ($MifFiles.Count -gt 0) { $MifFiles | Remove-Item -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] $($MifFiles.Count) SMS .mif file(s)" $Script:Removed += $MifFiles.Count } else { Write-Log " [NOT FOUND] No SMS .mif files" $Script:NotFound++ } # ============================================================== Write-Step "STEP 9 - Remove MDM / co-management enrollment state" # ============================================================== # Log what enrollments exist before wiping $EnrollmentRoot = "HKLM:\SOFTWARE\Microsoft\Enrollments" if (Test-Path $EnrollmentRoot) { $Guids = Get-ChildItem $EnrollmentRoot -ErrorAction SilentlyContinue Write-Log " [FOUND] $($Guids.Count) MDM enrollment GUID(s):" foreach ($g in $Guids) { $etype = (Get-ItemProperty "$($g.PSPath)" -Name "EnrollmentType" -ErrorAction SilentlyContinue).EnrollmentType $upn = (Get-ItemProperty "$($g.PSPath)" -Name "UPN" -ErrorAction SilentlyContinue).UPN Write-Log " $($g.PSChildName) | Type=$etype | UPN=$upn" Remove-Item -Path $g.PSPath -Recurse -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] Enrollment GUID: $($g.PSChildName)" $Script:Removed++ } } else { Write-Log " [NOT FOUND] $EnrollmentRoot" $Script:NotFound++ } @( "HKLM:\SOFTWARE\Microsoft\Enrollments", "HKLM:\SOFTWARE\Microsoft\EnterpriseResourceManager", "HKLM:\SOFTWARE\Microsoft\PolicyManager\current", "HKLM:\SOFTWARE\Microsoft\PolicyManager\device", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MDMCommon", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MDM", "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\MDM", "HKLM:\SOFTWARE\Microsoft\CCM\CoManagementHandler", "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin", "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo", "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudExperienceHost", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE", "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\UnattendSettings" ) | ForEach-Object { Remove-RegKey $_ } # MDM enrollment scheduled tasks Write-Log " Checking MDM enrollment scheduled tasks..." $MdmTaskCount = 0 foreach ($TaskPath in @("\Microsoft\Windows\EnterpriseMgmt\","\Microsoft\Windows\EnterpriseMgmtNoncritical\")) { $tasks = Get-ScheduledTask -TaskPath $TaskPath -ErrorAction SilentlyContinue foreach ($t in $tasks) { Unregister-ScheduledTask -TaskName $t.TaskName -TaskPath $t.TaskPath -Confirm:$false -ErrorAction SilentlyContinue Write-Log " [REMOVED] MDM task: $($t.TaskPath)$($t.TaskName)" $MdmTaskCount++ $Script:Removed++ } } if ($MdmTaskCount -eq 0) { Write-Log " [NOT FOUND] No MDM enrollment scheduled tasks" } # MDM device certificates Write-Log " Checking MDM device certificates in LocalMachine\My..." $MdmCerts = Get-ChildItem "Cert:\LocalMachine\My" -ErrorAction SilentlyContinue | Where-Object { $_.Issuer -match "MS-Organization-Access|Microsoft Intune MDM Device CA|MDM" } if ($MdmCerts) { foreach ($cert in $MdmCerts) { Remove-Item -Path "Cert:\LocalMachine\My\$($cert.Thumbprint)" -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] Cert: $($cert.Subject) | Issuer: $($cert.Issuer) | Thumb: $($cert.Thumbprint)" $Script:Removed++ } } else { Write-Log " [NOT FOUND] No MDM certificates in LocalMachine\My" $Script:NotFound++ } # ============================================================== Write-Step "STEP 10 - Final dsregcmd /leave after full cleanup" # ============================================================== Write-Log " Running dsregcmd /leave (post-cleanup flush)" $LeaveOut2 = & dsregcmd.exe /leave 2>&1 $LeaveOut2 | ForEach-Object { Write-Log " dsregcmd: $($_.ToString().Trim())" } Write-Log " Exit code: $LASTEXITCODE" # ============================================================== Write-Step "STEP 11 - Remove AAD / ZTID identity artifacts" # ============================================================== # These are what cause OOBE to skip the Autopilot ZTID check # and enroll as a plain device instead of an Autopilot device. # NGC (Next Generation Credentials / Windows Hello) folders. # These hold TPM-bound device keys from the previous AAD join. # If present during OOBE, Windows reuses the old device identity # instead of performing a fresh Autopilot hardware hash lookup. Write-Log " Removing NGC (Windows Hello) credential folders" @( "C:\Windows\ServiceProfiles\LocalService\AppData\Local\Microsoft\NGC", "C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\NGC", "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\NGC" ) | ForEach-Object { Remove-LockedDirectory $_ } # Cloud AP plugin token cache. # Cached AAD access tokens that allow Windows to silently re-join # the same AAD device without going through the Autopilot ZTID flow. Write-Log " Removing Cloud AP plugin token cache" @( "C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Windows\CloudAPPlugin", "C:\Windows\ServiceProfiles\LocalService\AppData\Local\Microsoft\Windows\CloudAPPlugin", "C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\CloudAPPlugin" ) | ForEach-Object { Remove-LockedDirectory $_ } # Autopilot provisioning cache. # If a cached profile exists here from a prior enrollment, OOBE uses # it instead of fetching the current profile from the Autopilot service. # This can cause OOBE to apply an old/wrong profile or skip ZTID entirely. Write-Log " Removing Autopilot provisioning cache" @( "C:\Windows\Provisioning\Autopilot", "C:\ProgramData\Microsoft\Windows\DeviceManagementProvisioning" ) | ForEach-Object { Remove-LockedDirectory $_ } # Identity store cache - device credential store used for silent re-auth Write-Log " Removing identity store cache" @( "HKLM:\SOFTWARE\Microsoft\IdentityStore\Cache", "HKLM:\SOFTWARE\Microsoft\IdentityStore\LogonCache", "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CDJ" ) | ForEach-Object { Remove-RegKey $_ } # AAD identity cache folder Remove-LockedDirectory "C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\IdentityCache" # All AAD-related certificates (broader than just MDM certs). # The MS-Organization-Access cert is the device identity cert issued # during AAD join. If it survives sysprep, the device re-joins as the # same AAD object and Intune matches it to the old co-managed record. Write-Log " Removing AAD device identity certificates" $AadCertStores = @("Cert:\LocalMachine\My", "Cert:\LocalMachine\Root", "Cert:\LocalMachine\CA") foreach ($Store in $AadCertStores) { $Certs = Get-ChildItem $Store -ErrorAction SilentlyContinue | Where-Object { $_.Issuer -match "MS-Organization-Access|MS-Organization-P2P-Access|Microsoft Intune MDM Device CA|AAD|AzureAD|EnterpriseRegistration" } foreach ($Cert in $Certs) { Remove-Item -Path "$Store\$($Cert.Thumbprint)" -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] Cert [$Store]: $($Cert.Subject) | $($Cert.Thumbprint)" $Script:Removed++ } if (-not $Certs) { Write-Log " [NOT FOUND] No AAD certs in $Store" $Script:NotFound++ } } # ============================================================== Write-Step "STEP 12 - Remove TS post action, unattend, setup remnants" # ============================================================== Write-Log " Removing SMSTSPostAction registry value" Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\SMS\Task Sequence" -Name "SMSTSPostAction" -ErrorAction SilentlyContinue @("C:\Windows\Panther\Unattend.xml","C:\Windows\Panther\unattend") | ForEach-Object { if (Test-Path $_) { Remove-Item -Path $_ -Recurse -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] $_" $Script:Removed++ } else { Write-Log " [NOT FOUND] $_" $Script:NotFound++ } } Remove-ItemProperty -Path "HKLM:\SYSTEM\Setup" -Name "CmdLine" -ErrorAction SilentlyContinue Remove-ItemProperty -Path "HKLM:\SYSTEM\Setup" -Name "SetupType" -ErrorAction SilentlyContinue Write-Log " Cleared HKLM:\SYSTEM\Setup CmdLine and SetupType" $TagPath = "C:\Windows\Setup\Scripts\DisableCMDRequest.TAG" if (Test-Path $TagPath) { Remove-Item -Path $TagPath -Force -ErrorAction SilentlyContinue Write-Log " [REMOVED] DisableCMDRequest.TAG" $Script:Removed++ } else { Write-Log " [NOT FOUND] DisableCMDRequest.TAG" $Script:NotFound++ } # ============================================================== Write-Step "POST-FLIGHT - dsregcmd /status after full cleanup" # ============================================================== $DsregAfter = & dsregcmd.exe /status 2>&1 $DsregAfter | Where-Object { $_ -match 'AzureAdJoined|WorkplaceJoined|DomainJoined|TenantName|DeviceId|MDMUrl|EnterpriseJoined' } | ForEach-Object { Write-Log " $($_.Trim())" } # ============================================================== Write-Step "CLEANUP SUMMARY" # ============================================================== Write-Log " Items removed : $($Script:Removed)" Write-Log " Items not found : $($Script:NotFound)" Write-Log " Items failed : $($Script:Failed)" # ============================================================== Write-Step "SYSPREP" # ============================================================== Write-Log " Launching: sysprep.exe /oobe /reboot /quiet" $Sysprep = Start-Process ` -FilePath "C:\Windows\System32\Sysprep\Sysprep.exe" ` -ArgumentList "/oobe /reboot /quiet" ` -NoNewWindow -Wait -PassThru Write-Log " Sysprep exit code: $($Sysprep.ExitCode)" if ($Sysprep.ExitCode -ne 0) { Write-Log " Sysprep FAILED - dumping Panther log" -Level Warning $PantherLog = "C:\Windows\System32\Sysprep\Panther\setupact.log" if (Test-Path $PantherLog) { Get-Content $PantherLog -Tail 40 -ErrorAction SilentlyContinue | ForEach-Object { Add-Content -Path $LogPath -Value " [Panther] $_" -ErrorAction SilentlyContinue } } else { Write-Log " Panther log not found at $PantherLog" -Level Warning } } else { Write-Log " Sysprep completed successfully - rebooting into OOBE" } Write-Log "PrepareOOBE finished" '@ New-Item -ItemType Directory -Path "C:\Windows\Temp" -Force -ErrorAction SilentlyContinue | Out-Null New-Item -ItemType File -Path "C:\Windows\Temp\PrepareOOBE.ps1" -Value $ScriptBlock -Force | Out-Null Write-Output "PrepareOOBE.ps1 created at C:\Windows\Temp\PrepareOOBE.ps1" |