Stop the one-line install handing a standard user a way in
The bootstrap command staged its script into C:\Windows\Temp and then ran it elevated. That directory grants Users create-file with CREATOR OWNER full control, so somebody without admin rights can pre-plant the exact path the elevated run is about to execute. It now stages under Program Files, creates the directory with an explicit DACL when it is missing, and refuses to run a staged file that is not owned by an admin principal. A failed hardening is loud rather than silent: the run stops instead of carrying on with a file it could not protect. The token moved out of the query string into a header. In a query string it lands verbatim in the reverse proxy and IIS logs, which is a credential written to a file nobody thinks of as a secret store. The query form is still accepted, because bays provisioned before this change are holding install commands that use it, and it logs that it was used - without logging the token - so a site can see who still needs a re-issued command before turning it off with plugin.endpoints.bootstrapquerytoken. Two smaller things this also closes: A request with no token at all answered 400 naming the header, which told an anonymous caller the state of that switch and contradicted the module's own promise that anything unusable answers 404. No token is now 404. The 400 is kept for a token genuinely presented in the query form while the query form is off, which is a caller who needs to know their command is stale. The rollout documentation was republishing the vulnerable one-liner verbatim, so a tech following the runbook reinstated exactly what this fixes. Both documents now carry the current command. Also corrects the operator-facing copy for the token, which named a query argument and a header that do not exist: it is ?t= and X-Endpoint-Bootstrap-Token, per bootstrap.py's own constants. Anybody following the old text got a 404 with no reason given. Both switches added in this range are now declared with the other plugin settings and reachable from their settings page. Read through Setting.get but never declared, they were rows that did not exist until someone wrote one by hand, which on an air-gapped box means the feature was unreachable.
This commit is contained in:
@@ -292,17 +292,63 @@ says so when it finds an agent already installed.
|
||||
a new one if it is lost.
|
||||
3. On the target PC, open PowerShell **as administrator** and paste it:
|
||||
|
||||
**Copy the command the UI gives you.** Do not retype it from here and do not
|
||||
reuse one you saved from an older round - see the warning under this block. It
|
||||
looks like this:
|
||||
|
||||
```
|
||||
powershell -ExecutionPolicy Bypass -c "try { Invoke-WebRequest -UseBasicParsing 'https://<server>/api/endpoints/siteagent/bootstrap.ps1?t=<token>' -OutFile 'C:\Windows\Temp\shopdb-bootstrap.ps1'; & 'C:\Windows\Temp\shopdb-bootstrap.ps1' } finally { Remove-Item 'C:\Windows\Temp\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue }"
|
||||
powershell -ExecutionPolicy Bypass -c "try { New-Item -ItemType Directory -Force -Path 'C:\Program Files\ShopDB\bootstrap' | Out-Null; icacls 'C:\Program Files\ShopDB\bootstrap' /inheritance:r /grant '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' | Out-Null; if ((Get-Variable LASTEXITCODE -ValueOnly) -ne 0) { Write-Host 'Refusing to run: could not restrict C:\Program Files\ShopDB\bootstrap to administrators (icacls failed). Fix the directory or delete it, then run the command again.' -ForegroundColor Red; exit 6 }; Remove-Item 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue; Invoke-WebRequest -UseBasicParsing 'https://<server>/api/endpoints/siteagent/bootstrap.ps1' -Headers @{'X-Endpoint-Bootstrap-Token'='<token>'} -OutFile 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1'; if ((Get-Acl 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1').GetOwner([Security.Principal.SecurityIdentifier]).Value -notin @('S-1-5-32-544','S-1-5-18',[Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) { Write-Host 'Refusing to run: the staged bootstrap script is not owned by an administrator.' -ForegroundColor Red; exit 5 }; & 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' } finally { Remove-Item 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue }"
|
||||
```
|
||||
|
||||
It downloads the script, runs it and deletes it again. Do not shorten it back
|
||||
to `irm ... | iex`: Defender scores that shape as `Trojan:Win32/Commando.A!ml`
|
||||
and kills the process before anything runs, on a default install with no ASR
|
||||
rules. Paste it as one line into an elevated **cmd** or **PowerShell** window;
|
||||
it means the same in both. **Download .ps1** in the same block saves the script
|
||||
instead, for a PC where pasting a long line is awkward - it carries the same
|
||||
token, so treat the file the way you treat the command.
|
||||
It creates an admin-only staging directory, downloads the script into it, runs
|
||||
it and deletes it again. Do not shorten it back to `irm ... | iex`: Defender
|
||||
scores that shape as `Trojan:Win32/Commando.A!ml` and kills the process before
|
||||
anything runs, on a default install with no ASR rules. Paste it as one line
|
||||
into an elevated **cmd** or **PowerShell** window; it means the same in both.
|
||||
**Download .ps1** in the same block saves the script instead, for a PC where
|
||||
pasting a long line is awkward - it carries the same token, so treat the file
|
||||
the way you treat the command.
|
||||
|
||||
> **Do not use the old `?t=<token>` one-liner that staged into
|
||||
> `C:\Windows\Temp`.** Any standard user on the bay can create that exact path
|
||||
> first and wait for a tech to paste the command, which then runs the planted
|
||||
> file elevated; the token also lands in the web server's access log. The
|
||||
> command above stages under `C:\Program Files`, locks the directory to
|
||||
> Administrators and SYSTEM and stops if that fails, refuses to run a staged
|
||||
> file owned by anyone else, and sends the token in a header.
|
||||
>
|
||||
> The `?t=` form is still ACCEPTED, but only so a bay provisioned before this
|
||||
> change keeps working until its command is re-issued. Every such fetch logs a
|
||||
> server warning and writes an audit row against the token:
|
||||
>
|
||||
> ```
|
||||
> bootstrap token presented in the query string by <caller> with no admin
|
||||
> download marker, so this is an un-migrated bay running a command pasted
|
||||
> before the header form; ... re-issue the one-line install command
|
||||
> ```
|
||||
>
|
||||
> **`<caller>` is 127.0.0.1 on a stock Windows install.** IIS runs the app
|
||||
> through HttpPlatformHandler over loopback and sets no `X-Forwarded-For` of
|
||||
> its own; `deploy/windows/web.config` ships the rewrite rule that would add
|
||||
> one commented out, so waitress has nothing to rewrite `REMOTE_ADDR` from.
|
||||
> Check before you rely on the address: if these lines all say 127.0.0.1, the
|
||||
> warning still tells you un-migrated bays EXIST and how many fetches they
|
||||
> made, but not which bays. Get real addresses by enabling that rule (the
|
||||
> Windows installer's `-ClientIpSource direct`, which installs URL Rewrite and
|
||||
> allows `HTTP_X_FORWARDED_FOR` at server level); behind a further reverse
|
||||
> proxy leave it off and set `TRUSTED_PROXY_COUNT` instead. Without that, the
|
||||
> usable identifier is the token: each warning has a matching audit row on
|
||||
> `EndpointBootstrapToken` carrying the 12-character `tokenprefix`, so you can
|
||||
> at least tell which install round the stale command came from.
|
||||
>
|
||||
> Work that list down: mint a new token and hand out the new command. Clicking
|
||||
> **Download .ps1** yourself does NOT add to it - that link is marked
|
||||
> `&via=download` and logs at info, precisely so the warnings stay a list of
|
||||
> bays. Once the list is empty, clear **Accept the token in the URL** in
|
||||
> Settings -> Endpoints -> Build site agent -> One-line install
|
||||
> (`plugin.endpoints.bootstrapquerytoken`) to refuse the query form outright.
|
||||
> That also disables the Download .ps1 link and kills every command still in a
|
||||
> technician's hands, so do it deliberately and re-issue first.
|
||||
|
||||
The token is short-lived and only fetches the installer. It carries no shared
|
||||
key and provisions nothing by itself: the package it downloads is built with
|
||||
@@ -324,6 +370,15 @@ window to override). After a successful install it waits up to two minutes for
|
||||
the credential to land and prints `approved and enrolled` if an admin approves
|
||||
it in that window; still pending is a normal outcome, not a failure.
|
||||
|
||||
If it stops before any of that with a red **Refusing to run**, nothing was
|
||||
installed and nothing was downloaded. Two cases, both meaning look at the bay
|
||||
rather than retry:
|
||||
|
||||
| Exit | Message | What to do |
|
||||
|---|---|---|
|
||||
| 6 | could not restrict `C:\Program Files\ShopDB\bootstrap` to administrators | the ACL could not be set - usually the directory already exists with an owner or DACL you did not put there. Inspect it (`icacls`), delete it, run the command again. |
|
||||
| 5 | the staged bootstrap script is not owned by an administrator | something else wrote the staged file. Treat the bay as suspect, do not force past it. |
|
||||
|
||||
Revoke a token from the same block when the install round is done. Tokens list
|
||||
with their prefix, expiry and uses; they are also spent automatically
|
||||
(`usecount` reaching `maxuses`) and expire on their own.
|
||||
|
||||
Reference in New Issue
Block a user