diff --git a/scan/adint_scan/modules/host.py b/scan/adint_scan/modules/host.py index e3ab5aa..f143dd4 100644 --- a/scan/adint_scan/modules/host.py +++ b/scan/adint_scan/modules/host.py @@ -59,7 +59,20 @@ except ImportError: # pragma: no cover - non-Windows winreg = None # type: ignore -_PS = ["powershell", "-NoProfile", "-NonInteractive", +# Resolve PowerShell by its absolute path, never the bare name. Invoked as +# "powershell", Windows resolves it through the executable search order - which +# includes the CURRENT DIRECTORY before System32. The tool tells users to run it +# from their Downloads folder, a user-writable location, so a planted +# powershell.exe there would be executed in its place (issue #7). An absolute +# path is never resolved through that search, so only the real interpreter at +# the System32 location can run; if it is not there, the call fails to a gap +# rather than ever executing a same-named file from elsewhere. +_PS_EXE = os.path.join(os.environ.get("SystemRoot") or r"C:\Windows", + "System32", "WindowsPowerShell", "v1.0", "powershell.exe") +# A directory the scanner does not control, used as the child's working +# directory so it cannot inherit a hostile CWD either. +_PS_CWD = os.environ.get("SystemRoot") or r"C:\Windows" +_PS = [_PS_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"] _DNS_LOG = "Microsoft-Windows-DNS-Client/Operational" @@ -142,7 +155,8 @@ def _ps(cmd: str, timeout: int = 60): # nothing to repair in the first place. cmd = "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; " + cmd proc = subprocess.run(_PS + [cmd], capture_output=True, text=True, - encoding="utf-8", errors="replace", timeout=timeout) + encoding="utf-8", errors="replace", timeout=timeout, + cwd=_PS_CWD) return proc.returncode, (proc.stdout or ""), (proc.stderr or "") diff --git a/scan/tests/test_known_paths.py b/scan/tests/test_known_paths.py index 2d25cfb..1805ad3 100644 --- a/scan/tests/test_known_paths.py +++ b/scan/tests/test_known_paths.py @@ -84,3 +84,24 @@ def test_same_named_but_different_file_is_a_note_not_a_hit(self): if __name__ == "__main__": unittest.main(verbosity=2) + + +# Regression for issue #7: PowerShell must be invoked by absolute path, never the +# bare name "powershell". Launched by bare name, Windows resolves it through the +# executable search order, which includes the current directory - letting a +# planted Downloads\powershell.exe run during the host checks (and forge their +# results). An absolute path is never resolved through that search. +import os as _os +from adint_scan.modules import host as _host + + +def test_powershell_is_an_absolute_path_not_a_bare_name(): + assert _os.path.isabs(_host._PS[0]), \ + "PowerShell must be an absolute path (issue #7), got: {!r}".format(_host._PS[0]) + assert _host._PS[0].lower().endswith("powershell.exe") + assert _host._PS[0] != "powershell" + + +def test_ps_runs_in_a_controlled_working_directory(): + # The child must not inherit a hostile CWD either. + assert _os.path.isabs(_host._PS_CWD)