CVE-2025-60690
6.1 Ghidra
get_merge_ipaddr
get_merge_ipaddr
Refined get_merge_ipaddr
get_merge_ipaddr...
Analysis
Keep in mind that we are working with the MIPS assembly for this binary. The insrtuctions and concepts are different from common x86, x64 or ARM assembly
We can see that the param_1 and param_2 variables corresponds to the 1st and 2nd arguments passed to the get_merge_ipaddr function. As what we can understand from MIPS assembly, this relates to the a0 and a1 registers in the current function (get_merge_ipaddr)
However, this does not necessarily mean the same for the nested function calls within the current get_merge_ipaddr function (eg. strcat, get_cgi)
Hence, there may be a situation where the disassembly listing view shows a particular param_x variable (corresponding to ax register) passed in as an argument to a nested function call, but that does not actually correspond to the x-positioned argument to that function call
Example 1
Lets take the following disassembly + decompiled code snippet:
From the instruction:
We can see that the value in register v0 is moved to the variable param_2, and this variable is then passed as the 1st argument to the strcat function (as seen from the decompiled code)
We might think that the value in register v0 is used as the 1st argument to the strcat function. However, in reality, this register is actually moved to the a1 register instead, which relates to the 2nd argument passed to the strcat function
Other findings
We can discover that the 2nd argument to the get_merge_ipaddr function is passed in as the 1st argument to the nested strcat function. Take a look at the following disassembly code (objdump):
(#1) Move
a1tos1
Moves the value of the 2nd argument to the
get_merge_ipaddrfunction to thes1register
(#2) Move
s1toa0
a0(value froms1) indicates the 1st argument to the following function call (strcat)
(#3) Calls the
strcatfunction (address stored in temporary registert9)
Important notes from each section
Disassembly
The
a0toanregisters indicates the exact values passed to each of the nested function calls
Decompiled code
The
param_1andparam_2variables indicates the 1st and 2nd argument to theget_merge_ipaddrfunctionsHowever, even though the 2nd argument (
param_2) to theget_merge_ipaddrfunction is shown to be passed as the 1st argument to thestrcatfunction, this is not actually the caseparam_2refers toa1, which instead relates to the 2nd argument passed to the nestedstrcatfunction call instead
Hence, the source of truth of argument passing should come from the a0, a1, an, etc. registers in the disassembly, rather than the decompiled code
6.2 gdb, gdbserver
gdb, gdbserver 6.2.1 Compile gdbserver for the target's architecture
gdbserver for the target's architectureTo start off, we have to retrieve the gdbserver binary for our target architecture mipsel (mips little-endian):
Due to limited space on the device, we can only store a few selected binaries at once. In this case, this will be gdbserver.
Remember to always check the available memory space available on the device, before transferring files
6.2.2 gdbserver on target router
gdbserver on target routerNext, we can run gdbserver on the target router:
We can use the steps outlined in the previous steps to transfer the
gdbserverbinary from host to the device
Alternatively, launch from PID of a running program:
6.2.3 Connect to remote target from host machine
To debug (using
gdb) from the host machineSince we know that our router uses the
mipselarchitecture (simplymipswith little-endian), we have to set it up appropriately
6.2.4 Attempting to overwrite return address
Refer to the following disassembly snippet:
In an attempt to overwrite the return address to invoke an RCE, we first have to identify the address stored in the s1 (location we write) register, and the location where ra (return address to overwrite) is saved
To achieve this, we can can print the values of each register at specific instructions:
racan be found at address$sp+96as seen from instruction 41f900s1@ right before 41f994
The value stored in
s1is written to thea0register (1st argument to strcat)
Calculate the distance between address stored in s1 and location where ra is saved:
6.3 Further enumeration
6.3.1 Ghidra
Window -> Function call graph
Window -> Function call tree
Window -> Defined strings
Right-click -> Data -> ... eg. TerminatedCString
Right-click -> References -> show TerminatedCstring
Right-click -> References -> show references to ...
...


6.3.2 Fuzzing
From UART console
Listen with
gdbserver
It appears that sending a VALID POST request to
/apply.cgitriggers a certain system reset (as seen from the UART console), with the following logs:
Only a VALID POST request parameter will trigger this behavior, while an invalid one (such as
--data "action=Apple") will not:
Interesting observation
For the POST request with an invalid action parameter passed to the
--data "action=xxxx"option, we get a response string:
Searching for the value "invalid" in the
apply_cgifunction (Ghidra), we find the following code snippet:
A more comprehensive code snippet:
Other valid values appears to be "Restore" and "Reboot"
Breakpoint does not work
There appears to be some form of protection from the device kernel or other feature that prevents breakpoints:
Using hardware breakpoint fails too:
...
Last updated