Skip to content

Commit d866152

Browse files
authored
Merge pull request #2966 from HackTricks-wiki/research_update_src_macos-hardening_macos-security-and-privilege-escalation_macos-proces-abuse_macos-vim-applications-injection_20260927_164558
Research Update Enhanced src/macos-hardening/macos-security-...
2 parents e6ed1d2 + f3eea9a commit d866152

1 file changed

Lines changed: 61 additions & 10 deletions

File tree

‎src/macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-vim-applications-injection.md‎

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ echo "hi" > /tmp/victim.txt
1717
printf ':qa!\n' | VIMINIT='silent! !touch /tmp/vim-executed' vim /tmp/victim.txt
1818
ls -la /tmp/vim-executed
1919

20-
# Pure Vimscript (no external process, e.g. write a file)
21-
printf ':qa!\n' | VIMINIT='call writefile(["x"],"/tmp/vim-vimscript")' vim /tmp/victim.txt
20+
# Pure Vimscript: write the marker and exit without reading stdin
21+
VIMINIT='call writefile(["x"], "/tmp/vim-vimscript")|qall!' vim /tmp/victim.txt
2222
```
2323

24-
The `:qa!` fed on stdin just closes the editor after the payload has already run; in a real scenario the victim simply opens Vim normally.
24+
The `:qa!` fed on stdin in the first example only closes the editor after the payload has run; in a real scenario the victim can open Vim normally.
25+
26+
`VIMINIT` is parsed as **one Ex command line**. Separate a chain with `|` (or a literal newline). It has precedence over the user's vimrc and `EXINIT`, so a payload does not need a malicious configuration file and runs before the normal user configuration.<sup>[[1]](#references)[[2]](#references)</sup>
2527

2628
## `EXINIT`
2729

@@ -32,20 +34,69 @@ printf ':qa!\n' | EXINIT='silent! !touch /tmp/exinit-executed' vim /tmp/victim.t
3234
ls -la /tmp/exinit-executed
3335
```
3436

37+
## Startup suppression and exploitability
38+
39+
This primitive depends on a **normal startup**. `vim -u NONE` / `nvim -u NONE` skip the environment/user initialization (and plugins), while `-u <file>` uses that file instead. Vim `-es`/`-Es` and Neovim `-es`, `-Es`, or `-l` also skip these initialization steps. Do not mistake `--headless` for a safe mode: a normal Neovim headless startup still processes `VIMINIT`.<sup>[[1]](#references)[[2]](#references)</sup>
40+
41+
Consequently, validate the complete launch chain: the variable must survive the wrapper, `sudo` policy, job runner, and editor selection, and the final command must not force `-u NONE`/`NORC` or batch mode. A reliable payload can terminate itself with `|qall!`, which also makes testing wrappers that do not provide a TTY easier.<sup>[[1]](#references)[[2]](#references)</sup>
42+
43+
## Neovim current-directory Lua module hijacking
44+
45+
A separate Neovim injection primitive affects builds whose Lua `package.path`/`package.cpath` still contain current-directory templates such as `./?.lua` or `./?.so`. Starting Neovim alone is insufficient: a config or plugin must call `require("name")`, and no earlier loader may resolve that name. A common trigger is an **optional dependency check** such as `pcall(require, "optional_dep")`; placing `optional_dep.lua` in an attacker-controlled working directory then executes it without enabling the separate `'exrc'` local-configuration feature. Core `vim.*` modules and modules already found on `'runtimepath'` are not generally shadowable, so enumerate actual missing/optional `require()` calls rather than guessing names.<sup>[[3]](#references)</sup>
46+
47+
The following reproduces the loader primitive with a harmless marker:<sup>[[3]](#references)</sup>
48+
49+
```bash
50+
mkdir -p /tmp/nvim-cwd-hijack
51+
cat > /tmp/nvim-cwd-hijack/optional_dep.lua <<'LUA'
52+
vim.fn.writefile({"loaded"}, "/tmp/nvim-cwd-hit")
53+
return {}
54+
LUA
55+
56+
cd /tmp/nvim-cwd-hijack
57+
nvim --clean --headless '+lua require("optional_dep")' +qa
58+
cat /tmp/nvim-cwd-hit
59+
```
60+
61+
Check the running build instead of relying only on a version string:<sup>[[3]](#references)</sup>
62+
63+
```bash
64+
nvim --clean --headless '+lua io.write(package.path)' +qa 2>&1 | tr ';' '\n'
65+
```
66+
67+
Upstream tracks removal of the current-directory fallback during normal editor startup while retaining Lua-script (`nvim -l`) behavior. Until the installed build no longer exposes it, place this at the **start** of `init.lua` (it intentionally removes relative current-directory Lua/C module templates, so do not apply it to workflows that require them):<sup>[[3]](#references)</sup>
68+
69+
```lua
70+
local function drop_cwd(path)
71+
local keep = {}
72+
for entry in path:gmatch("[^;]+") do
73+
if not entry:match("^%./") then keep[#keep + 1] = entry end
74+
end
75+
return table.concat(keep, ";")
76+
end
77+
package.path = drop_cwd(package.path)
78+
package.cpath = drop_cwd(package.cpath)
79+
```
80+
3581
## Notes and caveats
3682

37-
- **Neovim** honours `VIMINIT` too (it is checked before the user `init.vim`/`init.lua`).
38-
- Batch/Ex mode (`vim -es` / `vim -Es`) does **not** source `VIMINIT`/`EXINIT`; the variables run in a normal (interactive) startup, which is the common victim scenario.
39-
- Related file-based vectors are the per-directory `exrc`/`.nvimrc` "modeline"/local-rc features and `-u <vimrc>`; the environment-variable path above needs no writable file at all.
83+
- **Neovim** honours both `VIMINIT` and the `EXINIT` fallback, but its normal user configuration is `init.vim` or `init.lua`.<sup>[[2]](#references)</sup>
84+
- The environment-variable path needs no writable file. Local rc and current-directory module hijacking are separate, file-backed primitives.<sup>[[1]](#references)[[3]](#references)</sup>
85+
- Project-local configuration is a different surface from modelines. With Vim's `'exrc'` enabled, a local vimrc/exrc owned by another user runs with `'secure'` restrictions; however, extracting an archive normally makes the planted file owned by the victim and defeats that ownership-based protection. Neovim also searches for `.nvim.lua`, `.nvimrc`, or `.exrc` when `'exrc'` is enabled—do not conflate that opt-in mechanism with the `require()` current-directory fallback above.<sup>[[1]](#references)[[2]](#references)[[3]](#references)</sup>
86+
- Editor-selection variables only choose what program is launched; they do not guarantee that `VIMINIT` reaches the final process. Inspect the exact environment and arguments at the Vim/Neovim exec boundary.<sup>[[1]](#references)[[2]](#references)</sup>
4087

4188
## Hardening
4289

43-
- Sanitize the environment (drop `VIMINIT`/`EXINIT`) before launching editors from privileged or automated contexts, and prefer `sudo -i`/`env -i` wrappers that reset the environment.
44-
- Set `EDITOR`/`VISUAL` to trusted absolute paths and avoid running editors as root with an inherited user environment.
45-
- Treat control over a target's environment as equivalent to code execution for any Vim/Neovim it spawns.
90+
- Explicitly drop the variables before privileged or automated editor launches: `env -u VIMINIT -u EXINIT /usr/bin/vim -u NONE -- "$file"`. `-u NONE` is important when the caller must ignore every user startup source.<sup>[[1]](#references)[[2]](#references)</sup>
91+
- Set `EDITOR`/`VISUAL` to trusted absolute paths, avoid running interactive editors as root with an inherited user environment, and ensure wrappers cannot restore `VIMINIT`/`EXINIT` after sanitization.<sup>[[1]](#references)[[2]](#references)</sup>
92+
- For Neovim, update to a build that removes current-directory Lua/C search templates during editor mode, or strip them before loading plugins. Audit plugin code for optional `pcall(require, ...)` calls when opening untrusted repositories.<sup>[[3]](#references)</sup>
93+
- Treat control over a target's editor environment, working directory, or startup configuration as a potential code-execution primitive in the editor's security context.<sup>[[1]](#references)[[2]](#references)[[3]](#references)</sup>
94+
95+
4696

4797
## References
4898

4999
- [1] [Vim documentation — `starting.txt` (initialization, `VIMINIT`, `EXINIT`)](https://vimhelp.org/starting.txt.html#initialization)
50-
100+
- [2] [Neovim documentation — startup and initialization](https://neovim.io/doc/user/starting/)
101+
- [3] [Neovim issue #38966 — current-directory fallback in `require()`](https://github.com/neovim/neovim/issues/38966)
51102
{{#include ../../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)