MikroTik's Silent Patch Decoded: From SSH Username '-2' to Full RouterOS Admin
A deep dive into RouterOS 7.23.4 reveals a silent security fix that turns a read-only SSH session into full admin via a legacy file-descriptor login transport.
On September 3, 2026, MikroTik pushed RouterOS 7.23.4, 7.24.2, and 6.49.21 with a terse advisory: "important security update... not currently publishing detailed information." Translation: they found something nasty and are withholding details until the fleet patches. But as Nick Pratley points out, shipping fixed binaries to everyone makes the diff the disclosure. He pulled both versions and reversed the delta—in six hours, with AI assistance.
The one changelog line that matters
Across all three branches, a single coordinated entry stands out: *) ssh - refactor SSH internal processes and improved system stability; Present in the patched releases, absent from 7.23.3. That's the fingerprint of a serious fix.
Extracting the NPK
RouterOS ships as NPK files. The x86 base package is about 20MB, no auth needed. Inside is a squashfs 4.0 with xz compression. Carve it out and unsquash it—a throwaway Alpine container handles the missing unsquashfs.
RouterOS is a swarm of small 'nova' processes under /nova/bin/, brokered by a master loader. The SSH server lives in a bundle, and the client and server are the same binary. Key files: sshd (byte-identical to ssh), libucrypto.so (crypto), and libumsg.so (message bus + login handling).
Symbol-level diffing beats byte-level noise
A naive cmp reports 170KB of differences because inserted bytes shift addresses. The signal is in the symbol table. Diffing dynamic symbols across changed ELFs reveals the story: libumsg.so gains validLoginParamInput, libucrypto.so changes parseHashFromDerEncoded, and mtget adds snprintf and a 'Filename too long' error.
Piece one: '-2' is a file descriptor, not a username
The new validLoginParamInput rejects values starting with - or space, ending with space, or containing control bytes. But the real sink is /nova/bin/login, which has a legacy transport: if a positional argument starts with -, it strips the dash, parses the rest with atoi, and reads up to 4096 bytes from that file descriptor. For interactive SSH, fds 0, 1, and 2 are duplicates of the PTY slave. So a username of -2 means 'read the trusted login positionals from stderr/PTY fd 2'.
The second field becomes the authorization policy mask via strtoul. A PTY in canonical mode complicates the framing: you need two NUL-terminated fields followed by two VEOF bytes (0x04 0x04) to terminate the read cleanly.
0\x00 654958\x00 \x04\x04This yields a full policy mask of 0x9fe6e (654958), RouterOS's normalized full admin set.
Reproduced: read-only to full admin
Using a disposable RADIUS responder restricted to username -2, the outer session recorded as group read. After the fd-2 block, the console opened with full policy. The proof: :put [/system identity get name] returned CHR, then a write operation changed the identity to minus2-proof and back.
The validator is not cosmetic—it closes this trusted-argv/policy injection. The mtget overflow is a separate, independently reachable memory-corruption primitive. The low-exponent RSA signature forgery into mtget remains a third bug, not yet fully reproduced in a credential-free way.
If you ship the fixed binaries to the entire planet, then the diff between old and new is the disclosure.
| Release | Date | SSH fix | Other fixes |
|---|---|---|---|
| 7.23.4 (long-term) | 2026-09-03 | Yes | mtget overflow, RSA signature forgery |
| 7.24.2 (stable) | 2026-09-03 | Yes | Same as above |
| 6.49.21 (v6) | 2026-09-03 | Yes | Backported SSH fix only |
| 7.23.3 (previous) | Pre-2026-09-03 | No | Vulnerable |
Source: Nick Pratley
Discussion
0 Comments
Be the first to start the discussion.