User Minus One Is Root — Java Bug Hunt

Modelled on sudo CVE-2019-14287 (October 2019): with a sudoers rule meant to allow running a command as any user except root ((ALL, !root)), asking for user…

  • Language: Java
  • Layer: Backend
  • Difficulty: Easy
  • Concepts: Security, Auth, Overflow
  • Modelled on: sudo · CVE-2019-14287
  • Visible tests: an ordinary user id is allowed; user id -1 is denied
  • Reward: 50 XP for a complete fix

Briefing

Modelled on sudo CVE-2019-14287 (October 2019): with a sudoers rule meant to allow running a command as any user except root ((ALL, !root)), asking for user id -1 (or its unsigned form 4294967295) passed the "not root" check — and because the system call used to switch ids treats -1 as "leave this id unchanged", the command ran with sudo's own id: root.

RunAs.java reconstructs the runas step: parse #<uid>, check the policy, switch ids through Kernel.setresuid.

Fix RunAs.run so user id -1 can never be requested.

Bug report

BUG-SUDO-14287 · Priority: Critical (privilege escalation) · Reported by: security

run(spec) returns "uid=<n>" for the effective uid the command runs as, or "denied".

  • spec must be "#" followed by a decimal integer, else "denied"
  • values are 32-bit uids: a negative n means n + 2^32 (so "#-2" is 4294967294); values below -2^31 or above 4294967295 are "denied"
  • 4294967295 (-1) is not a user id — it is setresuid's "unchanged" marker — and must be "denied", however it is written
  • the policy (RunasPolicy.allows) must then approve the uid

Observed: run("#-1") returns "uid=0" — the policy said "not root" and the command ran as root.

Logs

[sudo] alice : TTY=pts/0 ; USER=#-1 ; COMMAND=/usr/bin/id
[sudo] policy (ALL, !root): target uid 4294967295 != 0 -> allowed
[id] uid=0(root) gid=1000(alice)

The code as shipped

RunAs.java (editable)

class RunAs {
    static String run(String spec) {
        if (spec == null || !spec.startsWith("#")) return "denied";
        long n;
        try {
            n = Long.parseLong(spec.substring(1));
        } catch (NumberFormatException e) {
            return "denied";
        }
        if (n < -2147483648L || n > 0xFFFFFFFFL) return "denied";
        long uid = n & 0xFFFFFFFFL;
        if (!RunasPolicy.allows(uid)) return "denied";
        return "uid=" + Kernel.setresuid(uid);
    }
}

Read-only context: Kernel.java, RunasPolicy.java.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Java bug hunts.