The Backslash at the End of the Line — Python Bug Hunt
Modelled on Baron Samedit (CVE-2021-3156, disclosed by Qualys in January 2021): when sudo ran in shell mode it unescaped backslashes in the command-line…
- Language: Python
- Layer: Backend
- Difficulty: Hard
- Concepts: Security, Overflow, Parsing
- Modelled on: sudo · CVE-2021-3156
- Visible tests: an escaped space is unescaped; a trailing backslash stays inside the argument
- Reward: 50 XP for a complete fix
Briefing
Modelled on Baron Samedit (CVE-2021-3156, disclosed by Qualys in January 2021): when sudo ran in shell mode it unescaped backslashes in the command-line arguments into a heap buffer sized from the argument's length. An argument ending in a single backslash made the loop step over the backslash and the string's NUL terminator, so it kept copying the next string in memory past the end of the buffer — a heap overflow any local user could trigger, present in sudo for about ten years.
unescape.py reconstructs the copy loop over a simulated heap (heap.py) in which strings sit back to back, each ended by a NUL, and the destination buffer records an out-of-bounds write the way C would silently allow it.
Fix unescape so it never reads past the argument's terminator or writes past its buffer.
Bug report
BUG-SAMEDIT · Priority: Critical (local privilege escalation) · Reported by: security
unescape(mem, start) copies the NUL-terminated argument at mem[start] into a FixedBuffer of capacity strlen + 1 and returns that buffer:
- a backslash followed by any character copies just that character ("a\ b" -> "a b", "\\" -> one backslash)
- a backslash that is the LAST character of the argument (immediately before its NUL) is copied literally as a backslash
- the copy ends with a single NUL; nothing after the argument's own terminator is ever read or written
- buf.overflowed must stay False; buf.text() is the unescaped argument
Observed: an argument ending in a backslash pulls the neighbouring heap string into the buffer and writes past its end.
Logs
[sudoedit] argv[1]="echo\" strlen=5 capacity=6
[heap] FixedBuffer(6) write #7 beyond capacity
[heap] FixedBuffer(6) wrote 15 charsThe code as shipped
src/sudo/unescape.py (editable)
heap = bug_require("src/sudo/heap.py")
BACKSLASH = chr(92)
def strlen(mem, start):
n = 0
while mem[start + n] != heap.NUL:
n += 1
return n
def unescape(mem, start):
"""Copies the argument at mem[start] into a fresh buffer, dropping the
backslash that escapes each shell metacharacter."""
buf = heap.FixedBuffer(strlen(mem, start) + 1)
i = start
while mem[i] != heap.NUL:
if mem[i] == BACKSLASH:
i += 1
buf.put(mem[i])
i += 1
buf.put(heap.NUL)
return buf
Read-only context: src/sudo/heap.py.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Python bug hunts.