From f0d454e9ad33b984734c4fb4fafe2197f70d9563 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 21:45:54 +0000 Subject: [PATCH 1/4] Fix bypass.ps1 crash, strip dead answer-file payload, refresh docs for 2026 bypass.ps1 had a guaranteed crash on its failure path: Write-Error does not take -ForegroundColor, so the final error handler threw a ParameterBindingException instead of reporting the problem. The file also carried two duplicate comment-based help blocks from an earlier merge. Rewrote it with the things a script that reboots your machine should have: a confirmation prompt (with -Force to skip), -WhatIf support via ShouldProcess, TLS 1.2 for images that still default to TLS 1.0, download to a temp file with XML validation before anything lands in C:\Windows\Panther, a backup of any existing answer file, and a pointer to setuperr.log when Sysprep refuses to run. unattend.xml went from 512 to 100 lines. Sysprep /oobe without /generalize does not re-run the specialize pass, so the ~40 app removals, 16 capability removals and all the script-extraction machinery parked there never executed - while making the file look like it debloated the machine. Removed rather than left in place looking functional. The oobeSystem pass, which is what actually performs the bypass, is kept and now also carries HideOnlineAccountScreens/HideLocalAccountScreen, plus self-contained FirstLogonCommands that no longer depend on files the specialize pass was supposed to have written. README rechecked against the current state (August 2026): ms-cxh:localonly is now blocked in stable, not just Insider, so the previous "only works on the current 25H2 stable branch" note was out of date. Added the working alternatives (Rufus, domain join on Pro, the BypassNRO registry value and its caveat) and documented putting the file on the USB as autounattend.xml, which avoids Sysprep and the second OOBE pass entirely. Also documented what was previously unstated: both accounts are created without a password and Admin autologs in once. Added the missing LICENSE, SECURITY.md, .gitignore and a PSScriptAnalyzer settings file, and wired the settings into CI. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Pt5Q3XvjMT6HMdubvkyLMW --- .github/PSScriptAnalyzerSettings.psd1 | 20 + .github/workflows/powershell.yml | 4 +- .gitignore | 14 + LICENSE | 21 + README.md | 135 +++++- SECURITY.md | 35 ++ bypass.ps1 | 291 +++++++----- unattend.xml | 612 +++++--------------------- 8 files changed, 505 insertions(+), 627 deletions(-) create mode 100644 .github/PSScriptAnalyzerSettings.psd1 create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 SECURITY.md diff --git a/.github/PSScriptAnalyzerSettings.psd1 b/.github/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..3dc58c5 --- /dev/null +++ b/.github/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,20 @@ +@{ + # Rules excluded for BypassNRO: + # + # PSAvoidUsingWriteHost -- this is an interactive tool run from the OOBE + # Shift+F10 console, where coloured status output + # is the point. There is no pipeline to pollute. + ExcludeRules = @( + 'PSAvoidUsingWriteHost' + ) + + # The script targets Windows PowerShell 5.1, which is what Shift+F10 + # provides during OOBE. + Rules = @{ + PSUseCompatibleCmdlets = @{ + compatibility = @( + 'desktop-5.1.14393.206-windows' + ) + } + } +} diff --git a/.github/workflows/powershell.yml b/.github/workflows/powershell.yml index f4a3dde..a5b9aaf 100644 --- a/.github/workflows/powershell.yml +++ b/.github/workflows/powershell.yml @@ -35,11 +35,9 @@ jobs: uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f with: # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. - # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. path: .\ recurse: true - # Include your own basic security rules. Removing this option will run all the rules - # includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' + settings: .github/PSScriptAnalyzerSettings.psd1 output: results.sarif # Upload the SARIF file generated in the previous step diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53c5b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Windows +Thumbs.db +desktop.ini +$RECYCLE.BIN/ + +# Editors +.vscode/ +.idea/ +*.swp + +# Local test output +*.log +*.bak +*.bak-* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..adda56a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Stensel8 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f4b8d7e..afd9ae4 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,134 @@ # Bypass NRO -## Status of Bypass Methods (December 2025) +Set up Windows 11 with a local account, using Windows' own answer-file mechanism instead of a console trick that Microsoft can remove. -Since March 2025, Microsoft's `oobe\bypassnro` command has been removed from Windows 11 (24H2/25H2). Until October 2025, the alternative `start ms-cxh:localonly` (and `start ms-cxh://setaddlocalonly`) worked, but this was blocked starting with Insider Build 26220.6772 (October 6, 2025) and only works on the current 25H2 stable branch. +```powershell +iex (irm bypassnro.stensel.nl) +``` + +Run from an elevated prompt. During OOBE, **Shift+F10** gives you one. + +--- + +## Status of bypass methods + +**Last verified: August 2026.** Microsoft has removed the console-based bypasses one by one; the answer-file route is the one that has held. + +| Method | Status | Notes | +|---|---|---| +| **`unattend.xml` / `autounattend.xml`** (this project) | **Works** | Part of Windows' supported deployment tooling. Microsoft cannot remove it without breaking enterprise imaging, Autopilot and MDT/SCCM. | +| **Rufus** ("Remove requirement for an online Microsoft account") | **Works** | Rufus patches the install media rather than using an OOBE trick, so it is unaffected by the OOBE changes. | +| **Domain join** — "Set up for work or school" → "Sign-in options" → "Domain join instead" | **Works on Pro/Enterprise** | Not available on Home. Microsoft has been narrowing this path, so treat it as a fallback. | +| **`start ms-cxh:localonly`** | **Blocked** | Blocked from Insider build 26220.6772 (6 October 2025) and since rolled into stable. | +| **`oobe\bypassnro`** | **Removed** | The script was deleted from the image in March 2025. | +| **`BypassNRO` registry value** | **Build-dependent** | `reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f` re-enables the flow, but only on builds where the underlying code is still present. Unreliable on current builds. | + +**Timeline** + +- **March 2025** — `oobe\bypassnro` removed from Windows 11 24H2/25H2. +- **6 October 2025** — `ms-cxh:localonly` blocked in Insider build 26220.6772, then in stable. +- **August 2026** — answer files, Rufus and (on Pro) domain join still reach a local account. No single console command works on every image any more. + +Microsoft keeps changing this. If something here stops working, open an issue. + +--- + +## Two ways to use it -The **BypassNRO method in this project still works** because it uses Sysprep with a custom unattend.xml. This approach remains functional for now, because unattend.xml is part of Windows' official enterprise deployment tools and cannot easily be blocked by Microsoft without breaking enterprise scenarios. +### Option 1 — On the installation media (recommended) -**Timeline:** -- Removal of `oobe\bypassnro`: March 2025 -- Blocking of `ms-cxh:localonly`: October 6, 2025 (Insider build 26220.6772 and higher) +Put the answer file on the USB **before** installing. Setup reads it automatically and never shows the account screen, so there is nothing to bypass and no second trip through OOBE. -## Download and Run (Shift+F10 during OOBE) +1. Create a Windows 11 USB (Media Creation Tool, Rufus, or by extracting the ISO). +2. Download [`unattend.xml`](unattend.xml) and save it to the **root of the USB** as `autounattend.xml`. +3. Boot from the USB and install as normal. + +This is faster and more reliable than Option 2, because Sysprep never has to run. + +### Option 2 — During OOBE, on a machine that is already installed + +Use this when you are already staring at the "Sign in with Microsoft" screen. + +1. Press **Shift+F10** to open a command prompt. +2. Run: + + ```powershell + powershell -c "iex (irm bypassnro.stensel.nl)" + ``` + +3. Confirm the prompt. The machine reboots and comes back through OOBE with the local accounts already created. + +Under the hood this downloads [`unattend.xml`](unattend.xml) to `C:\Windows\Panther\unattend.xml` and runs: + +``` +Sysprep.exe /oobe /unattend:C:\Windows\Panther\unattend.xml /reboot +``` + +**Parameters.** `iex` cannot pass arguments. Use a script block if you need them: -### PowerShell ```powershell -iex(irm bypassnro.stensel.nl) +# Skip the confirmation prompt +& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -Force + +# Shut down instead of rebooting +& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -NoReboot + +# See what it would do without doing it +& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -WhatIf ``` -### CMD Wrapper +--- + +## What the answer file does + +Only the **oobeSystem** pass is used. Defining local accounts there is what performs the bypass: OOBE skips the account screens because the accounts already exist, so it never asks for a Microsoft account. + +| Account | Group | Password | +|---|---|---| +| `Admin` | Administrators | *(none)* | +| `User` | Users | *(none)* | + +`Admin` is signed in automatically once, then autologon is switched off and the answer file is deleted from `C:\Windows\Panther`. + +> [!WARNING] +> **Both accounts are created without a password, and `Admin` logs in automatically.** That is what makes the bypass work, but it means the machine is wide open until you fix it. **Set a password immediately after the first sign-in** (`Settings > Accounts > Sign-in options`, or `net user Admin *`). + +### Why the answer file has an empty `specialize` pass + +`Sysprep /oobe` without `/generalize` does **not** re-run the specialize pass — only `oobeSystem` is processed. Anything placed in `specialize` (app removal, registry tweaks, script extraction) silently never runs in this flow. Earlier versions of this file carried a large debloat payload there that never executed. It has been removed rather than left in place looking functional. + +If you want debloating and tweaks, do it after setup with something built for it, such as [WinDeploy](https://github.com/Stensel8/WinDeploy) or [WinUtil](https://github.com/ChrisTitusTech/winutil). + +--- + +## Troubleshooting + +**"This script must be run elevated"** +Shift+F10 during OOBE already gives you an elevated prompt. Outside OOBE, start PowerShell as Administrator. + +**Script blocked by execution policy** ```powershell -powershell -c "iex(irm bypassnro.stensel.nl)" -``` \ No newline at end of file +Set-ExecutionPolicy Bypass -Scope Process -Force +``` + +**Sysprep exits non-zero** +Sysprep refused to run. Check `C:\Windows\System32\Sysprep\Panther\setuperr.log`. The usual causes are a pending reboot, an in-progress Windows Update, or Sysprep having already run the maximum number of times on this image. + +**Download fails** +The OOBE environment may have no network yet. Connect Ethernet (or use `Shift+F10` → `netsh wlan` to join Wi-Fi), or use Option 1 instead, which needs no network at all. + +--- + +## Requirements + +- Windows 11 (24H2 / 25H2 and later) +- Windows PowerShell 5.1 — what Shift+F10 provides +- Administrator rights + +## Disclaimer + +Provided as is, without warranty. Sysprep reboots the machine and sends it back through OOBE; anything unsaved is lost. Test before using on a machine you care about. + +## Licence + +[MIT](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..309aa1a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security Policy + +## Supported versions + +Only the `main` branch is supported. Use the current version of `bypass.ps1` +and `unattend.xml`. + +## Reporting a vulnerability + +Report vulnerabilities privately through +[GitHub Security Advisories](https://github.com/Stensel8/bypassnro/security/advisories/new). + +Please do not open a public issue for a security problem. + +## Scope + +This project writes a Windows answer file and runs Sysprep. Things worth +reporting: + +- A way to make `bypass.ps1` fetch or execute content from somewhere other + than the configured `UnattendUrl`. +- A flaw that leaves the answer file (which contains plaintext passwords) on + disk after first logon. +- Anything that grants more privilege than the documented behaviour. + +## Known and intended behaviour + +These are documented trade-offs, not vulnerabilities: + +- `unattend.xml` creates the `Admin` and `User` accounts **without a + password**, and signs `Admin` in automatically once. This is what makes the + bypass work. Set a password immediately after first logon. +- The one-liner (`iex (irm bypassnro.stensel.nl)`) downloads and executes a + remote script. Read `bypass.ps1` before running it if that matters to you. +- Sysprep reboots the machine and sends it back through OOBE. diff --git a/bypass.ps1 b/bypass.ps1 index e958ff8..d696b28 100644 --- a/bypass.ps1 +++ b/bypass.ps1 @@ -1,100 +1,191 @@ -<# -.SYNOPSIS - Downloads an unattend.xml from this repo and runs Sysprep /oobe /unattend: /reboot - - Designed for PowerShell 5.1 (Windows PowerShell). Requires elevation. - - USAGE: - - Ensure you run: Set-ExecutionPolicy Bypass -Scope Process -Force - - Run the script from an elevated PowerShell session. -#> -<# -.SYNOPSIS -Downloads an unattend.xml from this repo and runs Sysprep /oobe /unattend: /reboot - -Designed for PowerShell 5.1 (Windows PowerShell). Requires elevation. - -USAGE: - - Ensure you run: Set-ExecutionPolicy -Scope Process Bypass -Force - - Run the script from an elevated PowerShell session. -#> - -[CmdletBinding()] -param( - [string]$UnattendUrl = 'https://raw.githubusercontent.com/Stensel8/bypassnro/main/unattend.xml', - [string]$Destination = 'C:\Windows\Panther\unattend.xml' -) - -function Assert-Elevation { - if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error 'This script must be run elevated (as Administrator). Exiting.' - exit 1 - } -} - -function Save-Unattend { - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)][string]$Url, - [Parameter(Mandatory=$true)][string]$OutPath - ) - - $dir = Split-Path -Path $OutPath -Parent - if (-not (Test-Path -Path $dir)) { - New-Item -Path $dir -ItemType Directory -Force | Out-Null - } - - Write-Host "Downloading unattend file from: $Url" -ForegroundColor Cyan - try { - # Use Invoke-WebRequest; fallback to Start-BitsTransfer if available - Invoke-WebRequest -Uri $Url -UseBasicParsing -OutFile $OutPath -ErrorAction Stop - } catch { - Write-Warning "Invoke-WebRequest failed: $($_.Exception.Message). Trying Start-BitsTransfer..." - try { - Start-BitsTransfer -Source $Url -Destination $OutPath -ErrorAction Stop - } catch { - Write-Error "Failed to download $Url - $($_.Exception.Message)" - exit 2 - } - } - - if (-not (Test-Path -Path $OutPath)) { - Write-Error "Download completed but file not found at $OutPath" - exit 3 - } -} - -function Start-Sysprep { - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)][string]$UnattendPath - ) - - $sysprep = Join-Path -Path 'C:\Windows' -ChildPath 'System32\Sysprep\Sysprep.exe' - if (-not (Test-Path -Path $sysprep)) { - Write-Error "Sysprep not found at $sysprep" - exit 4 - } - - $argumentList = @('/oobe', "/unattend:`"$UnattendPath`"", '/reboot') - Write-Host "Running: $sysprep $($argumentList -join ' ')" -ForegroundColor Yellow - $proc = Start-Process -FilePath $sysprep -ArgumentList $argumentList -Wait -PassThru - - if ($proc.ExitCode -ne 0) { - Write-Error "Sysprep exited with code $($proc.ExitCode)" - exit $proc.ExitCode - } -} - -Assert-Elevation - -Write-Host "Using destination: $Destination" -ForegroundColor Green -Save-Unattend -Url $UnattendUrl -OutPath $Destination - -if (Test-Path -Path $Destination) { - Write-Host "Unattend file saved to $Destination" -ForegroundColor Green - Start-Sysprep -UnattendPath $Destination -} else { - Write-Error "Failed to place unattend file at $Destination" -ForegroundColor Red - exit 5 -} +<# +.SYNOPSIS + Sets up Windows 11 with a local account by applying an unattend.xml through Sysprep. + +.DESCRIPTION + Downloads an answer file from this repository, writes it to + C:\Windows\Panther\unattend.xml and runs: + + Sysprep.exe /oobe /unattend: /reboot + + On the next boot, OOBE processes the answer file's oobeSystem pass, which + creates local accounts and skips the Microsoft-account sign-in screens. + + Unlike `oobe\bypassnro` (removed in March 2025) and `ms-cxh:localonly` + (blocked from October 2025), unattend.xml is part of Windows' supported + deployment tooling, so it is not something Microsoft can remove without + breaking enterprise imaging. + +.PARAMETER UnattendUrl + Answer file to download. Defaults to the copy in this repository. + +.PARAMETER Destination + Where to write the answer file. Defaults to C:\Windows\Panther\unattend.xml. + +.PARAMETER Force + Skip the confirmation prompt. + +.PARAMETER NoReboot + Run Sysprep with /shutdown instead of /reboot. + +.EXAMPLE + iex (irm bypassnro.stensel.nl) + + Run from an elevated prompt (Shift+F10 during OOBE gives you one). + +.EXAMPLE + & ([scriptblock]::Create((irm bypassnro.stensel.nl))) -Force + + Same, without the confirmation prompt. `iex` cannot pass parameters, so use + a script block when you need them. + +.NOTES + Requires elevation. Designed for Windows PowerShell 5.1, which is what + Shift+F10 gives you during OOBE. + + THIS REBOOTS THE MACHINE and sends it back through OOBE. Any work in + progress is lost. +#> + +#Requires -Version 5.1 + +[CmdletBinding()] +param( + [string]$UnattendUrl = 'https://raw.githubusercontent.com/Stensel8/bypassnro/main/unattend.xml', + [string]$Destination = 'C:\Windows\Panther\unattend.xml', + [switch]$Force, + [switch]$NoReboot +) + +$ErrorActionPreference = 'Stop' + +# Invoke-WebRequest's progress bar makes downloads dramatically slower in +# Windows PowerShell, and it renders badly in the OOBE console. +$ProgressPreference = 'SilentlyContinue' + +function Assert-Elevation { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]$identity + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Error 'This script must be run elevated (as Administrator). Exiting.' + exit 1 + } +} + +function Save-Unattend { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][string]$Url, + [Parameter(Mandatory = $true)][string]$OutPath + ) + + $dir = Split-Path -Path $OutPath -Parent + if (-not (Test-Path -Path $dir)) { + New-Item -Path $dir -ItemType Directory -Force | Out-Null + } + + # Download to a temporary file first, so a failed or truncated transfer + # cannot leave a broken answer file in C:\Windows\Panther. + $temp = Join-Path ([System.IO.Path]::GetTempPath()) ("unattend-{0}.xml" -f [guid]::NewGuid()) + + Write-Host "Downloading answer file from: $Url" -ForegroundColor Cyan + try { + Invoke-WebRequest -Uri $Url -UseBasicParsing -OutFile $temp -ErrorAction Stop + } catch { + Write-Warning "Invoke-WebRequest failed: $($_.Exception.Message). Trying Start-BitsTransfer..." + try { + Start-BitsTransfer -Source $Url -Destination $temp -ErrorAction Stop + } catch { + Write-Error "Failed to download $Url - $($_.Exception.Message)" + exit 2 + } + } + + if (-not (Test-Path -Path $temp)) { + Write-Error "Download reported success but no file was written to $temp" + exit 3 + } + + # Make sure we got XML and not a captive-portal page or a GitHub error. + try { + $xml = New-Object System.Xml.XmlDocument + $xml.Load($temp) + if ($xml.DocumentElement.LocalName -ne 'unattend') { + throw "root element is <$($xml.DocumentElement.LocalName)>, expected " + } + } catch { + Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue + Write-Error "Downloaded file is not a valid unattend answer file: $($_.Exception.Message)" + exit 3 + } + + # Keep whatever was there before; Windows may already have an answer file. + if (Test-Path -LiteralPath $OutPath) { + $backup = "$OutPath.bak-{0}" -f (Get-Date -Format 'yyyyMMdd-HHmmss') + Copy-Item -LiteralPath $OutPath -Destination $backup -Force -ErrorAction SilentlyContinue + Write-Host "Existing answer file backed up to $backup" -ForegroundColor DarkGray + } + + Move-Item -LiteralPath $temp -Destination $OutPath -Force +} + +function Start-Sysprep { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)][string]$UnattendPath, + [switch]$Shutdown + ) + + $sysprep = Join-Path -Path $env:SystemRoot -ChildPath 'System32\Sysprep\Sysprep.exe' + if (-not (Test-Path -Path $sysprep)) { + Write-Error "Sysprep not found at $sysprep" + exit 4 + } + + $finish = if ($Shutdown) { '/shutdown' } else { '/reboot' } + $argumentList = @('/oobe', "/unattend:`"$UnattendPath`"", $finish) + + Write-Host "Running: $sysprep $($argumentList -join ' ')" -ForegroundColor Yellow + if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Run Sysprep /oobe $finish")) { + return + } + + $proc = Start-Process -FilePath $sysprep -ArgumentList $argumentList -Wait -PassThru + + # Sysprep restarts the machine itself, so a non-zero code here means it + # refused to run - check C:\Windows\System32\Sysprep\Panther\setuperr.log. + if ($proc.ExitCode -ne 0) { + Write-Error "Sysprep exited with code $($proc.ExitCode). See C:\Windows\System32\Sysprep\Panther\setuperr.log" + exit $proc.ExitCode + } +} + +Assert-Elevation + +# Windows PowerShell 5.1 can still default to TLS 1.0 on some images, which +# raw.githubusercontent.com refuses. +try { + [Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +} catch { + Write-Verbose "Could not raise the TLS version: $($_.Exception.Message)" +} + +if (-not $Force) { + Write-Host "" + Write-Host "This will run Sysprep and $(if ($NoReboot) { 'shut down' } else { 'restart' }) the computer." -ForegroundColor Yellow + Write-Host "Windows will go back through OOBE and create the local accounts" -ForegroundColor Yellow + Write-Host "defined in the answer file. Anything unsaved will be lost." -ForegroundColor Yellow + Write-Host "" + $answer = Read-Host "Continue? [y/N]" + if ($answer -notmatch '^(y|yes)$') { + Write-Host "Cancelled. Nothing was changed." -ForegroundColor Cyan + exit 0 + } +} + +Write-Host "Using destination: $Destination" -ForegroundColor Green +Save-Unattend -Url $UnattendUrl -OutPath $Destination +Write-Host "Answer file saved to $Destination" -ForegroundColor Green + +Start-Sysprep -UnattendPath $Destination -Shutdown:$NoReboot diff --git a/unattend.xml b/unattend.xml index 3e615a7..56977e3 100644 --- a/unattend.xml +++ b/unattend.xml @@ -1,512 +1,100 @@ - - - - - - - - - 00000-00000-00000-00000-00000 - OnError - - true - - false - - - - - - - - 1 - powershell.exe -WindowStyle Normal -NoProfile -Command "$xml = [xml]::new(); $xml.Load('C:\Windows\Panther\unattend.xml'); $sb = [scriptblock]::Create( $xml.unattend.Extensions.ExtractScript ); Invoke-Command -ScriptBlock $sb -ArgumentList $xml;" - - - 2 - powershell.exe -WindowStyle Normal -NoProfile -Command "Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\Specialize.ps1' -Raw | Invoke-Expression;" - - - 3 - reg.exe load "HKU\DefaultUser" "C:\Users\Default\NTUSER.DAT" - - - 4 - powershell.exe -WindowStyle Normal -NoProfile -Command "Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\DefaultUser.ps1' -Raw | Invoke-Expression;" - - - 5 - reg.exe unload "HKU\DefaultUser" - - - - - - - - - - - - Admin - - Administrators - - - true</PlainText> - </Password> - </LocalAccount> - <LocalAccount wcm:action="add"> - <Name>User</Name> - <DisplayName></DisplayName> - <Group>Users</Group> - <Password> - <Value></Value> - <PlainText>true</PlainText> - </Password> - </LocalAccount> - </LocalAccounts> - </UserAccounts> - <AutoLogon> - <Username>Admin</Username> - <Enabled>true</Enabled> - <LogonCount>1</LogonCount> - <Password> - <Value></Value> - <PlainText>true</PlainText> - </Password> - </AutoLogon> - <OOBE> - <ProtectYourPC>3</ProtectYourPC> - <HideEULAPage>true</HideEULAPage> - <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> - <HideOnlineAccountScreens>false</HideOnlineAccountScreens> - </OOBE> - <FirstLogonCommands> - <SynchronousCommand wcm:action="add"> - <Order>1</Order> - <CommandLine>powershell.exe -WindowStyle Normal -NoProfile -Command "Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\FirstLogon.ps1' -Raw | Invoke-Expression;"</CommandLine> - </SynchronousCommand> - </FirstLogonCommands> - </component> - </settings> - <Extensions xmlns="https://schneegans.de/windows/unattend-generator/"> - <ExtractScript> -param( - [xml] $Document -); - -foreach( $file in $Document.unattend.Extensions.File ) { - $path = [System.Environment]::ExpandEnvironmentVariables( $file.GetAttribute( 'path' ) ); - mkdir -Path( $path | Split-Path -Parent ) -ErrorAction 'SilentlyContinue'; - $encoding = switch( [System.IO.Path]::GetExtension( $path ) ) { - { $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8; } - { $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new( $false, $true ); } - default { [System.Text.Encoding]::Default; } - }; - $bytes = $encoding.GetPreamble() + $encoding.GetBytes( $file.InnerText.Trim() ); - [System.IO.File]::WriteAllBytes( $path, $bytes ); -} - </ExtractScript> - <File path="C:\Windows\Setup\Scripts\RemovePackages.ps1"> -$selectors = @( - 'Microsoft.Microsoft3DViewer'; - 'Microsoft.BingSearch'; - 'Microsoft.WindowsCamera'; - 'Clipchamp.Clipchamp'; - 'Microsoft.549981C3F5F10'; - 'Microsoft.Windows.DevHome'; - 'MicrosoftCorporationII.MicrosoftFamily'; - 'Microsoft.WindowsFeedbackHub'; - 'Microsoft.Edge.GameAssist'; - 'Microsoft.GetHelp'; - 'Microsoft.Getstarted'; - 'microsoft.windowscommunicationsapps'; - 'Microsoft.WindowsMaps'; - 'Microsoft.MixedReality.Portal'; - 'Microsoft.BingNews'; - 'Microsoft.WindowsNotepad'; - 'Microsoft.MicrosoftOfficeHub'; - 'Microsoft.Office.OneNote'; - 'Microsoft.OutlookForWindows'; - 'Microsoft.MSPaint'; - 'Microsoft.People'; - 'Microsoft.Windows.Photos'; - 'Microsoft.PowerAutomateDesktop'; - 'MicrosoftCorporationII.QuickAssist'; - 'Microsoft.SkypeApp'; - 'Microsoft.MicrosoftSolitaireCollection'; - 'Microsoft.MicrosoftStickyNotes'; - 'MicrosoftTeams'; - 'MSTeams'; - 'Microsoft.Todos'; - 'Microsoft.WindowsSoundRecorder'; - 'Microsoft.Wallet'; - 'Microsoft.BingWeather'; - 'Microsoft.Xbox.TCUI'; - 'Microsoft.XboxApp'; - 'Microsoft.XboxGameOverlay'; - 'Microsoft.XboxGamingOverlay'; - 'Microsoft.XboxIdentityProvider'; - 'Microsoft.XboxSpeechToTextOverlay'; - 'Microsoft.GamingApp'; - 'Microsoft.ZuneMusic'; - 'Microsoft.ZuneVideo'; -); -$getCommand = { - Get-AppxProvisionedPackage -Online; -}; -$filterCommand = { - $_.DisplayName -eq $selector; -}; -$removeCommand = { - [CmdletBinding()] - param( - [Parameter( Mandatory, ValueFromPipeline )] - $InputObject - ); - process { - $InputObject | Remove-AppxProvisionedPackage -AllUsers -Online -ErrorAction 'Continue'; - } -}; -$type = 'Package'; -$logfile = 'C:\Windows\Setup\Scripts\RemovePackages.log'; -&amp; { - $installed = &amp; $getCommand; - foreach( $selector in $selectors ) { - $result = [ordered] @{ - Selector = $selector; - }; - $found = $installed | Where-Object -FilterScript $filterCommand; - if( $found ) { - $result.Output = $found | &amp; $removeCommand; - if( $? ) { - $result.Message = "$type removed."; - } else { - $result.Message = "$type not removed."; - $result.Error = $Error[0]; - } - } else { - $result.Message = "$type not installed."; - } - $result | ConvertTo-Json -Depth 3 -Compress; - } -} *&gt;&amp;1 &gt;&gt; $logfile; - </File> - <File path="C:\Windows\Setup\Scripts\RemoveCapabilities.ps1"> -$selectors = @( - 'Print.Fax.Scan'; - 'Language.Handwriting'; - 'Browser.InternetExplorer'; - 'MathRecognizer'; - 'OneCoreUAP.OneSync'; - 'OpenSSH.Client'; - 'Microsoft.Windows.PowerShell.ISE'; - 'App.Support.QuickAssist'; - 'Language.Speech'; - 'Language.TextToSpeech'; - 'App.StepsRecorder'; - 'Hello.Face.18967'; - 'Hello.Face.Migration.18967'; - 'Hello.Face.20134'; - 'Media.WindowsMediaPlayer'; - 'Microsoft.Windows.WordPad'; -); -$getCommand = { - Get-WindowsCapability -Online | Where-Object -Property 'State' -NotIn -Value @( - 'NotPresent'; - 'Removed'; - ); -}; -$filterCommand = { - ($_.Name -split '~')[0] -eq $selector; -}; -$removeCommand = { - [CmdletBinding()] - param( - [Parameter( Mandatory, ValueFromPipeline )] - $InputObject - ); - process { - $InputObject | Remove-WindowsCapability -Online -ErrorAction 'Continue'; - } -}; -$type = 'Capability'; -$logfile = 'C:\Windows\Setup\Scripts\RemoveCapabilities.log'; -&amp; { - $installed = &amp; $getCommand; - foreach( $selector in $selectors ) { - $result = [ordered] @{ - Selector = $selector; - }; - $found = $installed | Where-Object -FilterScript $filterCommand; - if( $found ) { - $result.Output = $found | &amp; $removeCommand; - if( $? ) { - $result.Message = "$type removed."; - } else { - $result.Message = "$type not removed."; - $result.Error = $Error[0]; - } - } else { - $result.Message = "$type not installed."; - } - $result | ConvertTo-Json -Depth 3 -Compress; - } -} *&gt;&amp;1 &gt;&gt; $logfile; - </File> - <File path="C:\Windows\Setup\Scripts\RemoveFeatures.ps1"> -$selectors = @( - 'MediaPlayback'; - 'MicrosoftWindowsPowerShellV2Root'; - 'Microsoft-RemoteDesktopConnection'; - 'Recall'; -); -$getCommand = { - Get-WindowsOptionalFeature -Online | Where-Object -Property 'State' -NotIn -Value @( - 'Disabled'; - 'DisabledWithPayloadRemoved'; - ); -}; -$filterCommand = { - $_.FeatureName -eq $selector; -}; -$removeCommand = { - [CmdletBinding()] - param( - [Parameter( Mandatory, ValueFromPipeline )] - $InputObject - ); - process { - $InputObject | Disable-WindowsOptionalFeature -Online -Remove -NoRestart -ErrorAction 'Continue'; - } -}; -$type = 'Feature'; -$logfile = 'C:\Windows\Setup\Scripts\RemoveFeatures.log'; -&amp; { - $installed = &amp; $getCommand; - foreach( $selector in $selectors ) { - $result = [ordered] @{ - Selector = $selector; - }; - $found = $installed | Where-Object -FilterScript $filterCommand; - if( $found ) { - $result.Output = $found | &amp; $removeCommand; - if( $? ) { - $result.Message = "$type removed."; - } else { - $result.Message = "$type not removed."; - $result.Error = $Error[0]; - } - } else { - $result.Message = "$type not installed."; - } - $result | ConvertTo-Json -Depth 3 -Compress; - } -} *&gt;&amp;1 &gt;&gt; $logfile; - </File> - <File path="C:\Windows\Setup\Scripts\SetStartPins.ps1"> -$json = '{"pinnedList":[]}'; -if( [System.Environment]::OSVersion.Version.Build -lt 20000 ) { - return; -} -$key = 'Registry::HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start'; -New-Item -Path $key -ItemType 'Directory' -ErrorAction 'SilentlyContinue'; -Set-ItemProperty -LiteralPath $key -Name 'ConfigureStartPins' -Value $json -Type 'String'; - </File> - <File path="C:\Windows\Setup\Scripts\Specialize.ps1"> -$scripts = @( - { - Remove-Item -LiteralPath 'Registry::HKLM\Software\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' -Force -ErrorAction 'SilentlyContinue'; - }; - { - reg.exe add "HKCR\.txt\ShellNew" /v ItemName /t REG_EXPAND_SZ /d "@C:\Windows\system32\notepad.exe,-470" /f; - reg.exe add "HKCR\.txt\ShellNew" /v NullFile /t REG_SZ /f; - reg.exe add "HKCR\txtfilelegacy" /v FriendlyTypeName /t REG_EXPAND_SZ /d "@C:\Windows\system32\notepad.exe,-469" /f; - reg.exe add "HKCR\txtfilelegacy" /ve /t REG_SZ /d "Text Document" /f; - }; - { - Remove-Item -LiteralPath 'C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk', 'C:\Windows\System32\OneDriveSetup.exe', 'C:\Windows\SysWOW64\OneDriveSetup.exe' -ErrorAction 'Continue'; - }; - { - Remove-Item -LiteralPath 'Registry::HKLM\Software\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' -Force -ErrorAction 'SilentlyContinue'; - }; - { - reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" /v ConfigureChatAutoInstall /t REG_DWORD /d 0 /f; - }; - { - Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\RemovePackages.ps1' -Raw | Invoke-Expression; - }; - { - Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\RemoveCapabilities.ps1' -Raw | Invoke-Expression; - }; - { - Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\RemoveFeatures.ps1' -Raw | Invoke-Expression; - }; - { - net.exe accounts /maxpwage:UNLIMITED; - }; - { - reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f - }; - { - reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Dsh" /v AllowNewsAndInterests /t REG_DWORD /d 0 /f; - }; - { - Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\SetStartPins.ps1' -Raw | Invoke-Expression; - }; -); - -&amp; { - [float] $complete = 0; - [float] $increment = 100 / $scripts.Count; - foreach( $script in $scripts ) { - Write-Progress -Activity 'Running scripts to customize your Windows installation. Do not close this window.' -PercentComplete $complete; - '*** Will now execute command &#xAB;{0}&#xBB;.' -f $( - $str = $script.ToString().Trim() -replace '\s+', ' '; - $max = 100; - if( $str.Length -le $max ) { - $str; - } else { - $str.Substring( 0, $max - 1 ) + '&#x2026;'; - } - ); - $start = [datetime]::Now; - &amp; $script; - '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; - "`r`n" * 3; - $complete += $increment; - } -} *&gt;&amp;1 &gt;&gt; "C:\Windows\Setup\Scripts\Specialize.log"; - </File> - <File path="C:\Windows\Setup\Scripts\UserOnce.ps1"> -$scripts = @( - { - Get-AppxPackage -Name 'Microsoft.Windows.Ai.Copilot.Provider' | Remove-AppxPackage; - }; - { - Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Search' -Name 'SearchboxTaskbarMode' -Type 'DWord' -Value 0; - }; - { - Get-Process -Name 'explorer' -ErrorAction 'SilentlyContinue' | Where-Object -FilterScript { - $_.SessionId -eq ( Get-Process -Id $PID ).SessionId; - } | Stop-Process -Force; - }; -); - -&amp; { - [float] $complete = 0; - [float] $increment = 100 / $scripts.Count; - foreach( $script in $scripts ) { - Write-Progress -Activity 'Running scripts to configure this user account. Do not close this window.' -PercentComplete $complete; - '*** Will now execute command &#xAB;{0}&#xBB;.' -f $( - $str = $script.ToString().Trim() -replace '\s+', ' '; - $max = 100; - if( $str.Length -le $max ) { - $str; - } else { - $str.Substring( 0, $max - 1 ) + '&#x2026;'; - } - ); - $start = [datetime]::Now; - &amp; $script; - '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; - "`r`n" * 3; - $complete += $increment; - } -} *&gt;&amp;1 &gt;&gt; "$env:TEMP\UserOnce.log"; - </File> - <File path="C:\Windows\Setup\Scripts\DefaultUser.ps1"> -$scripts = @( - { - reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\WindowsCopilot" /v TurnOffWindowsCopilot /t REG_DWORD /d 1 /f; - }; - { - reg.exe add "HKU\DefaultUser\Software\Microsoft\Internet Explorer\LowRegistry\Audio\PolicyConfig\PropertyStore" /f; - }; - { - reg.exe add "HKU\DefaultUser\Software\Microsoft\Notepad" /v ShowStoreBanner /t REG_DWORD /d 0 /f; - }; - { - Remove-ItemProperty -LiteralPath 'Registry::HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'OneDriveSetup' -Force -ErrorAction 'Continue'; - }; - { - reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\GameDVR" /v AppCaptureEnabled /t REG_DWORD /d 0 /f; - }; - { - reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v ShowTaskViewButton /t REG_DWORD /d 0 /f; - }; - { - reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v TaskbarAl /t REG_DWORD /d 0 /f; - }; - { - reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\Explorer" /v DisableSearchBoxSuggestions /t REG_DWORD /d 1 /f; - }; - { - reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v "UnattendedSetup" /t REG_SZ /d "powershell.exe -WindowStyle Normal -NoProfile -Command \""Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\UserOnce.ps1' -Raw | Invoke-Expression;\""" /f; - }; -); - -&amp; { - [float] $complete = 0; - [float] $increment = 100 / $scripts.Count; - foreach( $script in $scripts ) { - Write-Progress -Activity 'Running scripts to modify the default user&#x2019;&#x2019;s registry hive. Do not close this window.' -PercentComplete $complete; - '*** Will now execute command &#xAB;{0}&#xBB;.' -f $( - $str = $script.ToString().Trim() -replace '\s+', ' '; - $max = 100; - if( $str.Length -le $max ) { - $str; - } else { - $str.Substring( 0, $max - 1 ) + '&#x2026;'; - } - ); - $start = [datetime]::Now; - &amp; $script; - '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; - "`r`n" * 3; - $complete += $increment; - } -} *&gt;&amp;1 &gt;&gt; "C:\Windows\Setup\Scripts\DefaultUser.log"; - </File> - <File path="C:\Windows\Setup\Scripts\FirstLogon.ps1"> -$scripts = @( - { - Set-ItemProperty -LiteralPath 'Registry::HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name 'AutoLogonCount' -Type 'DWord' -Force -Value 0; - }; - { - cmd.exe /c "rmdir C:\Windows.old"; - }; - { - Remove-Item -LiteralPath @( - 'C:\Windows\Panther\unattend.xml'; - 'C:\Windows\Panther\unattend-original.xml'; - 'C:\Windows\Setup\Scripts\Wifi.xml'; - ) -Force -ErrorAction 'SilentlyContinue' -Verbose; - }; -); - -&amp; { - [float] $complete = 0; - [float] $increment = 100 / $scripts.Count; - foreach( $script in $scripts ) { - Write-Progress -Activity 'Running scripts to finalize your Windows installation. Do not close this window.' -PercentComplete $complete; - '*** Will now execute command &#xAB;{0}&#xBB;.' -f $( - $str = $script.ToString().Trim() -replace '\s+', ' '; - $max = 100; - if( $str.Length -le $max ) { - $str; - } else { - $str.Substring( 0, $max - 1 ) + '&#x2026;'; - } - ); - $start = [datetime]::Now; - &amp; $script; - '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; - "`r`n" * 3; - $complete += $increment; - } -} *&gt;&amp;1 &gt;&gt; "C:\Windows\Setup\Scripts\FirstLogon.log"; - </File> - </Extensions> -</unattend> +<?xml version="1.0" encoding="utf-8"?> +<!-- + BypassNRO answer file - https://github.com/Stensel8/bypassnro + + Creates local accounts and skips the Microsoft-account sign-in screens + during OOBE. Works two ways: + + 1. Sysprep (bypass.ps1) + Sysprep.exe /oobe /unattend:C:\Windows\Panther\unattend.xml /reboot + Only the oobeSystem pass is processed. The specialize pass is NOT + re-run without /generalize, which is why there is nothing in it. + + 2. Installation media (recommended) + Rename to autounattend.xml and put it on the root of the Windows 11 + USB. Setup picks it up automatically and both windowsPE and + oobeSystem are processed. This never reaches the account screen at + all, so there is nothing to bypass. + + WARNING: both accounts below are created WITHOUT A PASSWORD, and Admin is + logged on automatically once. Set a password immediately after first logon. +--> +<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"> + <settings pass="offlineServicing"></settings> + <settings pass="windowsPE"> + <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <UserData> + <ProductKey> + <!-- All zeroes: skip the product key page and use the firmware/digital licence. --> + <Key>00000-00000-00000-00000-00000</Key> + <WillShowUI>OnError</WillShowUI> + </ProductKey> + <AcceptEula>true</AcceptEula> + </UserData> + <UseConfigurationSet>false</UseConfigurationSet> + </component> + </settings> + <settings pass="generalize"></settings> + <settings pass="specialize"></settings> + <settings pass="auditSystem"></settings> + <settings pass="auditUser"></settings> + <settings pass="oobeSystem"> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <!-- + Defining LocalAccounts here is what actually performs the bypass: + OOBE skips the account screens entirely because the accounts already + exist, so it never asks for a Microsoft account. + --> + <UserAccounts> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Name>Admin</Name> + <DisplayName></DisplayName> + <Group>Administrators</Group> + <Password> + <Value></Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + <LocalAccount wcm:action="add"> + <Name>User</Name> + <DisplayName></DisplayName> + <Group>Users</Group> + <Password> + <Value></Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <AutoLogon> + <Username>Admin</Username> + <Enabled>true</Enabled> + <LogonCount>1</LogonCount> + <Password> + <Value></Value> + <PlainText>true</PlainText> + </Password> + </AutoLogon> + <OOBE> + <ProtectYourPC>3</ProtectYourPC> + <HideEULAPage>true</HideEULAPage> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideLocalAccountScreen>true</HideLocalAccountScreen> + </OOBE> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <Order>1</Order> + <Description>Disable autologon after the first sign-in</Description> + <CommandLine>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 0 /f</CommandLine> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>2</Order> + <Description>Remove the answer file, which contains the plaintext passwords</Description> + <CommandLine>cmd.exe /c del /q /f "C:\Windows\Panther\unattend.xml" "C:\Windows\Panther\unattend-original.xml"</CommandLine> + </SynchronousCommand> + </FirstLogonCommands> + </component> + </settings> +</unattend> From 0edc10e230737d101b895850ce60080275e1d552 Mon Sep 17 00:00:00 2001 From: Claude <claude.ai@stensel.nl> Date: Sat, 29 Aug 2026 22:53:07 +0000 Subject: [PATCH 2/4] Stop pinning TLS 1.2 DevSkim flags this as an error on the PR (DS440000 and DS440020, both "Do not hardcode SSL/TLS versions within an application", severity important). Verified against the rule definitions: DS440020 applies to PowerShell and matches SecurityProtocolType, DS440000 matches the Tls12 literal. The scanner is right. Windows 11 is the only supported target here, and it already negotiates TLS 1.2/1.3 through SystemDefault. Pinning a version only stops the OS from handing us a better protocol later. The TLS 1.0 default this was guarding against belongs to Windows 7/8.1-era images, which this script does not support. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pt5Q3XvjMT6HMdubvkyLMW --- bypass.ps1 | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bypass.ps1 b/bypass.ps1 index d696b28..30992a9 100644 --- a/bypass.ps1 +++ b/bypass.ps1 @@ -162,14 +162,10 @@ function Start-Sysprep { Assert-Elevation -# Windows PowerShell 5.1 can still default to TLS 1.0 on some images, which -# raw.githubusercontent.com refuses. -try { - [Net.ServicePointManager]::SecurityProtocol = - [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -} catch { - Write-Verbose "Could not raise the TLS version: $($_.Exception.Message)" -} +# No TLS version is pinned here on purpose. Windows 11 (the only supported +# target) already negotiates TLS 1.2/1.3 through SystemDefault, and hardcoding +# a version stops the OS handing us a better protocol later. Pinning is only +# needed on Windows 7/8.1-era images, which this script does not support. if (-not $Force) { Write-Host "" From 624ffe7b3b34084130e6b5a8105b9ea7606f4856 Mon Sep 17 00:00:00 2001 From: Claude <claude.ai@stensel.nl> Date: Sun, 30 Aug 2026 00:20:42 +0000 Subject: [PATCH 3/4] Soften the ms-cxh:localonly claim to match the evidence, clear DevSkim DS104456 Two unrelated corrections. I overstated the ms-cxh:localonly row. I wrote that the block had "since rolled into stable", but I never verified that. Every primary source was unreachable from this environment (the egress proxy blocks learn.microsoft.com, the tech press and the forums), so the claim rested on search-result summaries saying "not supported anymore on the latest Windows 11 builds" - which is vague, and the build numbers actually cited are Insider ones (Dev 26220.6772, Beta 26120.6772). The retail 25H2 branch is 26200.x, and whether the block reached it is exactly the part I could not confirm. The previous README said it still worked on stable, so this was a change to a factual claim on thin evidence. The row now says "Blocked on Insider; retail unconfirmed" and states plainly that it has not been re-tested. Added a short note on how current the table is, so readers can see which rows are verified and which are compiled from public reporting. Separately: DevSkim reported two errors on this PR, DS104456 "Use of restricted functions". Fetched the check-run annotations to get the exact lines rather than guessing - both were the bare token `iex` in the comment-based help, which DevSkim matches because it does not treat a <# #> block as a comment. Reworded both .EXAMPLE entries to the script-block form, which is what supports parameters anyway; the one-liner stays documented in the README, which was not flagged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pt5Q3XvjMT6HMdubvkyLMW --- README.md | 12 +++++++++--- bypass.ps1 | 7 ++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index afd9ae4..517b86e 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,23 @@ Run from an elevated prompt. During OOBE, **Shift+F10** gives you one. | **`unattend.xml` / `autounattend.xml`** (this project) | **Works** | Part of Windows' supported deployment tooling. Microsoft cannot remove it without breaking enterprise imaging, Autopilot and MDT/SCCM. | | **Rufus** ("Remove requirement for an online Microsoft account") | **Works** | Rufus patches the install media rather than using an OOBE trick, so it is unaffected by the OOBE changes. | | **Domain join** — "Set up for work or school" → "Sign-in options" → "Domain join instead" | **Works on Pro/Enterprise** | Not available on Home. Microsoft has been narrowing this path, so treat it as a fallback. | -| **`start ms-cxh:localonly`** | **Blocked** | Blocked from Insider build 26220.6772 (6 October 2025) and since rolled into stable. | +| **`start ms-cxh:localonly`** | **Blocked on Insider; retail unconfirmed** | Blocked in Insider Dev 26220.6772 / Beta 26120.6772 (6 October 2025). Whether that block has reached the retail 25H2 branch (26200.x) has **not** been re-tested for this repo — check your own image before relying on it either way. | | **`oobe\bypassnro`** | **Removed** | The script was deleted from the image in March 2025. | | **`BypassNRO` registry value** | **Build-dependent** | `reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f` re-enables the flow, but only on builds where the underlying code is still present. Unreliable on current builds. | **Timeline** - **March 2025** — `oobe\bypassnro` removed from Windows 11 24H2/25H2. -- **6 October 2025** — `ms-cxh:localonly` blocked in Insider build 26220.6772, then in stable. +- **6 October 2025** — `ms-cxh:localonly` blocked in Insider builds Dev 26220.6772 and Beta 26120.6772. - **August 2026** — answer files, Rufus and (on Pro) domain join still reach a local account. No single console command works on every image any more. -Microsoft keeps changing this. If something here stops working, open an issue. +### How current is this table? + +The answer-file route is the only row verified against this repo's own code. The +rest is compiled from public reporting, not from a test run on each build, and +Microsoft changes this often enough that a row can go stale between releases. +Treat the console-command rows as "last known state", not as a guarantee, and +open an issue if your image behaves differently. --- diff --git a/bypass.ps1 b/bypass.ps1 index 30992a9..1ec4298 100644 --- a/bypass.ps1 +++ b/bypass.ps1 @@ -29,15 +29,16 @@ Run Sysprep with /shutdown instead of /reboot. .EXAMPLE - iex (irm bypassnro.stensel.nl) + & ([scriptblock]::Create((irm bypassnro.stensel.nl))) Run from an elevated prompt (Shift+F10 during OOBE gives you one). + See the README for the shorter pipe-to-execute one-liner. .EXAMPLE & ([scriptblock]::Create((irm bypassnro.stensel.nl))) -Force - Same, without the confirmation prompt. `iex` cannot pass parameters, so use - a script block when you need them. + Same, without the confirmation prompt. The short one-liner form cannot pass + parameters, so use a script block when you need them. .NOTES Requires elevation. Designed for Windows PowerShell 5.1, which is what From 9a3be52e1ec523646f46cae1e371caaafdbc2ab9 Mon Sep 17 00:00:00 2001 From: Claude <claude.ai@stensel.nl> Date: Sun, 30 Aug 2026 16:04:56 +0000 Subject: [PATCH 4/4] Trim README and SECURITY.md to match repo style Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pt5Q3XvjMT6HMdubvkyLMW --- README.md | 141 +++++++++------------------------------------------- SECURITY.md | 40 ++++----------- 2 files changed, 34 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 517b86e..81eb7ed 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,45 @@ # Bypass NRO -Set up Windows 11 with a local account, using Windows' own answer-file mechanism instead of a console trick that Microsoft can remove. +## Status of Bypass Methods (August 2026) -```powershell -iex (irm bypassnro.stensel.nl) -``` - -Run from an elevated prompt. During OOBE, **Shift+F10** gives you one. - ---- - -## Status of bypass methods - -**Last verified: August 2026.** Microsoft has removed the console-based bypasses one by one; the answer-file route is the one that has held. - -| Method | Status | Notes | -|---|---|---| -| **`unattend.xml` / `autounattend.xml`** (this project) | **Works** | Part of Windows' supported deployment tooling. Microsoft cannot remove it without breaking enterprise imaging, Autopilot and MDT/SCCM. | -| **Rufus** ("Remove requirement for an online Microsoft account") | **Works** | Rufus patches the install media rather than using an OOBE trick, so it is unaffected by the OOBE changes. | -| **Domain join** — "Set up for work or school" → "Sign-in options" → "Domain join instead" | **Works on Pro/Enterprise** | Not available on Home. Microsoft has been narrowing this path, so treat it as a fallback. | -| **`start ms-cxh:localonly`** | **Blocked on Insider; retail unconfirmed** | Blocked in Insider Dev 26220.6772 / Beta 26120.6772 (6 October 2025). Whether that block has reached the retail 25H2 branch (26200.x) has **not** been re-tested for this repo — check your own image before relying on it either way. | -| **`oobe\bypassnro`** | **Removed** | The script was deleted from the image in March 2025. | -| **`BypassNRO` registry value** | **Build-dependent** | `reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f` re-enables the flow, but only on builds where the underlying code is still present. Unreliable on current builds. | - -**Timeline** - -- **March 2025** — `oobe\bypassnro` removed from Windows 11 24H2/25H2. -- **6 October 2025** — `ms-cxh:localonly` blocked in Insider builds Dev 26220.6772 and Beta 26120.6772. -- **August 2026** — answer files, Rufus and (on Pro) domain join still reach a local account. No single console command works on every image any more. - -### How current is this table? - -The answer-file route is the only row verified against this repo's own code. The -rest is compiled from public reporting, not from a test run on each build, and -Microsoft changes this often enough that a row can go stale between releases. -Treat the console-command rows as "last known state", not as a guarantee, and -open an issue if your image behaves differently. - ---- - -## Two ways to use it - -### Option 1 — On the installation media (recommended) - -Put the answer file on the USB **before** installing. Setup reads it automatically and never shows the account screen, so there is nothing to bypass and no second trip through OOBE. - -1. Create a Windows 11 USB (Media Creation Tool, Rufus, or by extracting the ISO). -2. Download [`unattend.xml`](unattend.xml) and save it to the **root of the USB** as `autounattend.xml`. -3. Boot from the USB and install as normal. - -This is faster and more reliable than Option 2, because Sysprep never has to run. - -### Option 2 — During OOBE, on a machine that is already installed - -Use this when you are already staring at the "Sign in with Microsoft" screen. +Since March 2025, Microsoft's `oobe\bypassnro` command has been removed from Windows 11 (24H2/25H2). The alternative `start ms-cxh:localonly` (and `start ms-cxh://setaddlocalonly`) was blocked starting with Insider build 26220.6772 (October 6, 2025). Whether that block reached the retail 25H2 branch (26200.x) has not been re-tested here, so check your own image before relying on it. -1. Press **Shift+F10** to open a command prompt. -2. Run: +The **BypassNRO method in this project still works** because it uses Sysprep with a custom unattend.xml. This approach remains functional for now, because unattend.xml is part of Windows' official enterprise deployment tools and cannot easily be blocked by Microsoft without breaking enterprise scenarios. - ```powershell - powershell -c "iex (irm bypassnro.stensel.nl)" - ``` +Rufus, and on Pro/Enterprise "Set up for work or school" > "Sign-in options" > "Domain join instead", also still reach a local account. -3. Confirm the prompt. The machine reboots and comes back through OOBE with the local accounts already created. +**Timeline:** +- Removal of `oobe\bypassnro`: March 2025 +- Blocking of `ms-cxh:localonly`: October 6, 2025 (Insider builds 26220.6772 / 26120.6772) -Under the hood this downloads [`unattend.xml`](unattend.xml) to `C:\Windows\Panther\unattend.xml` and runs: +## Download and Run (Shift+F10 during OOBE) +### PowerShell +```powershell +iex(irm bypassnro.stensel.nl) ``` -Sysprep.exe /oobe /unattend:C:\Windows\Panther\unattend.xml /reboot -``` - -**Parameters.** `iex` cannot pass arguments. Use a script block if you need them: +### CMD Wrapper ```powershell -# Skip the confirmation prompt -& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -Force - -# Shut down instead of rebooting -& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -NoReboot - -# See what it would do without doing it -& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -WhatIf +powershell -c "iex(irm bypassnro.stensel.nl)" ``` ---- - -## What the answer file does - -Only the **oobeSystem** pass is used. Defining local accounts there is what performs the bypass: OOBE skips the account screens because the accounts already exist, so it never asks for a Microsoft account. - -| Account | Group | Password | -|---|---|---| -| `Admin` | Administrators | *(none)* | -| `User` | Users | *(none)* | - -`Admin` is signed in automatically once, then autologon is switched off and the answer file is deleted from `C:\Windows\Panther`. - -> [!WARNING] -> **Both accounts are created without a password, and `Admin` logs in automatically.** That is what makes the bypass work, but it means the machine is wide open until you fix it. **Set a password immediately after the first sign-in** (`Settings > Accounts > Sign-in options`, or `net user Admin *`). - -### Why the answer file has an empty `specialize` pass - -`Sysprep /oobe` without `/generalize` does **not** re-run the specialize pass — only `oobeSystem` is processed. Anything placed in `specialize` (app removal, registry tweaks, script extraction) silently never runs in this flow. Earlier versions of this file carried a large debloat payload there that never executed. It has been removed rather than left in place looking functional. - -If you want debloating and tweaks, do it after setup with something built for it, such as [WinDeploy](https://github.com/Stensel8/WinDeploy) or [WinUtil](https://github.com/ChrisTitusTech/winutil). - ---- - -## Troubleshooting - -**"This script must be run elevated"** -Shift+F10 during OOBE already gives you an elevated prompt. Outside OOBE, start PowerShell as Administrator. - -**Script blocked by execution policy** +`iex` cannot pass parameters. Use a script block for those: ```powershell -Set-ExecutionPolicy Bypass -Scope Process -Force +& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -Force # skip confirmation +& ([scriptblock]::Create((irm bypassnro.stensel.nl))) -NoReboot # shut down instead ``` -**Sysprep exits non-zero** -Sysprep refused to run. Check `C:\Windows\System32\Sysprep\Panther\setuperr.log`. The usual causes are a pending reboot, an in-progress Windows Update, or Sysprep having already run the maximum number of times on this image. - -**Download fails** -The OOBE environment may have no network yet. Connect Ethernet (or use `Shift+F10` → `netsh wlan` to join Wi-Fi), or use Option 1 instead, which needs no network at all. - ---- +## Without Sysprep (faster) -## Requirements +Save [`unattend.xml`](unattend.xml) to the root of the Windows 11 USB as `autounattend.xml`. Setup reads it during installation, so OOBE never asks for an account and there is no second reboot. -- Windows 11 (24H2 / 25H2 and later) -- Windows PowerShell 5.1 — what Shift+F10 provides -- Administrator rights +## Accounts -## Disclaimer +`unattend.xml` creates `Admin` (Administrators) and `User` (Users), both **without a password**, and signs `Admin` in automatically once. Set a password right after first logon. -Provided as is, without warranty. Sysprep reboots the machine and sends it back through OOBE; anything unsaved is lost. Test before using on a machine you care about. +## Notes -## Licence +Only the `oobeSystem` pass applies. `Sysprep /oobe` without `/generalize` does not re-run `specialize`, so anything placed there is ignored. For debloating and tweaks use [WinDeploy](https://github.com/Stensel8/WinDeploy) or [WinUtil](https://github.com/ChrisTitusTech/winutil). -[MIT](LICENSE) +Troubleshooting: Sysprep logs to `C:\Windows\System32\Sysprep\Panther\setuperr.log`. diff --git a/SECURITY.md b/SECURITY.md index 309aa1a..7e56dcb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,35 +1,17 @@ -# Security Policy +## Reporting a Vulnerability -## Supported versions +**Do not open a public GitHub issue for security vulnerabilities.** -Only the `main` branch is supported. Use the current version of `bypass.ps1` -and `unattend.xml`. +Report privately via [GitHub Security Advisories](https://github.com/Stensel8/bypassnro/security/advisories/new). -## Reporting a vulnerability +Include: +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Suggested fix (if any) -Report vulnerabilities privately through -[GitHub Security Advisories](https://github.com/Stensel8/bypassnro/security/advisories/new). +You will receive a response within 7 days. If the report is accepted, a fix will be released as soon as possible and you will be credited in the release notes. -Please do not open a public issue for a security problem. +### Out of scope -## Scope - -This project writes a Windows answer file and runs Sysprep. Things worth -reporting: - -- A way to make `bypass.ps1` fetch or execute content from somewhere other - than the configured `UnattendUrl`. -- A flaw that leaves the answer file (which contains plaintext passwords) on - disk after first logon. -- Anything that grants more privilege than the documented behaviour. - -## Known and intended behaviour - -These are documented trade-offs, not vulnerabilities: - -- `unattend.xml` creates the `Admin` and `User` accounts **without a - password**, and signs `Admin` in automatically once. This is what makes the - bypass work. Set a password immediately after first logon. -- The one-liner (`iex (irm bypassnro.stensel.nl)`) downloads and executes a - remote script. Read `bypass.ps1` before running it if that matters to you. -- Sysprep reboots the machine and sends it back through OOBE. +By design, `unattend.xml` creates accounts without a password and auto-logs in once, and the one-liner downloads and runs a remote script. These are documented in the README, not vulnerabilities.