The Empty Argument List That Became Root — Python Bug Hunt
Modelled on PwnKit (CVE-2021-4034, disclosed by Qualys in January 2022). polkit's pkexec assumed it was always started with at least one argument.
- Language: Python
- Layer: Backend
- Difficulty: Medium
- Concepts: Security, Bounds
- Modelled on: polkit · CVE-2021-4034
- Visible tests: a program name is resolved through the lookup; an empty argv is refused with ValueError
- Reward: 50 XP for a complete fix
Briefing
Modelled on PwnKit (CVE-2021-4034, disclosed by Qualys in January 2022). polkit's pkexec assumed it was always started with at least one argument. Started with an empty argv, its option loop was skipped, it read argv[1] anyway — which on Linux is the first environment variable, laid out right after argv — and later wrote the resolved program path back into that slot. That out-of-bounds read and write let a local user smuggle an environment variable past pkexec's sanitising and gain root. The bug had been present since 2009; the fix makes pkexec refuse to run when argc is 0.
process.py models the kernel's layout as one array (argv, a terminator, then envp) with unchecked indexing, like C. pkexec.py resolves the program to run.
Fix resolve so it never reads or writes outside argv.
Bug report
BUG-PWNKIT · Priority: Critical (local root) · Reported by: security
resolve(frame, lookup) returns {"program": path, "args": [...]}:
- argv[0] is pkexec itself; options start at argv[1] and are the arguments beginning with "--"; the first argument that is not an option is the program
- a program without a leading "/" is resolved with lookup(name) and the resolved path is written back into its argv slot
- "args" are the arguments after the program
- if frame.argc < 1, raise ValueError before touching anything
- if there is no program (only pkexec and options), raise ValueError
- nothing outside argv (indexes 0 .. argc-1) may ever be read or written
Observed: with an empty argv, resolve reads the first environment variable as if it were the program and overwrites it with a "resolved" path.
Logs
pkexec: argc=0 argv[1]="LANG=C"
pkexec: resolved program "/usr/bin/LANG=C" written to argv[1]
audit: environment of setuid process modified after sanitisingThe code as shipped
src/polkit/pkexec.py (editable)
# Resolves the program pkexec was asked to run.
def resolve(frame, lookup):
n = 1
while n < frame.argc:
if frame.argv(n).startswith("--"):
n += 1
continue
break
path = frame.argv(n)
if not path.startswith("/"):
path = lookup(path)
frame.set_argv(n, path)
args = [frame.argv(i) for i in range(n + 1, frame.argc)]
return {"program": path, "args": args}
Read-only context: src/polkit/process.py.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Python bug hunts.