Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions scan/adint_scan/modules/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 "")


Expand Down
21 changes: 21 additions & 0 deletions scan/tests/test_known_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading