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 chars

The 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.