Root, No Password, Second Try — JavaScript Bug Hunt

Modelled on the macOS High Sierra root login bug (CVE-2017-13872, November 2017): typing root with an empty password into an authentication dialog failed…

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Hard
  • Concepts: Security, Auth
  • Modelled on: macOS · CVE-2017-13872
  • Visible tests: a normal account checks its password; root with an empty password never gets in
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the macOS High Sierra root login bug (CVE-2017-13872, November 2017): typing root with an empty password into an authentication dialog failed the first time and succeeded the second. The root account ships disabled, with no password at all; the credential check, finding no stored password hash, took the path meant for upgrading legacy passwords and saved the one just typed. Apple described it as a logic error in the validation of credentials and shipped a security update within a day.

auth.js reconstructs that check. hashing.js holds the (toy) current hash and legacy formats.

Fix authenticate so a disabled account never authenticates and a login attempt never sets a password.

Bug report

BUG-ROOTLOGIN · Priority: Critical (privilege escalation) · Reported by: a developer on Twitter

directory: { <username>: { passwordHash: string|null, legacyCrypt: string|null } }

authenticate(directory, username, password):

  • unknown user -> false
  • an empty password never authenticates, for any account
  • passwordHash set -> hashing.verify(password, passwordHash)
  • only legacyCrypt set (an account from before the hash upgrade): if hashing.verifyLegacy(password, legacyCrypt) the account is upgraded (passwordHash = hashing.hash(password), legacyCrypt = null) and the login succeeds; otherwise false and nothing changes
  • neither set: the account is disabled -> false, and the record is never modified

Observed: root/"" fails once, then succeeds — and root now has a password.

Logs

[authd] authenticate root: no shadow hash, upgrading credential
[authd] authenticate root: FAILED
[authd] authenticate root: OK

The code as shipped

src/auth/auth.js (editable)

var hashing = require("./hashing");

exports.authenticate = function (directory, username, password) {
  var account = directory[username];
  if (!account) return false;
  if (!account.passwordHash) {
    var upgrading = !!account.legacyCrypt;
    if (upgrading && !hashing.verifyLegacy(password, account.legacyCrypt)) return false;
    account.passwordHash = hashing.hash(password);
    account.legacyCrypt = null;
    return upgrading;
  }
  return hashing.verify(password, account.passwordHash);
};

Read-only context: src/auth/ACCOUNTS.js, src/auth/hashing.js.

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