CVE-2025-60690
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
2. gdb, gdbserver
gdb, gdbserver Refer to the GDB, gdbserver section under the Expoit research page, for more information on the setup process
2.1 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:
3. Further enumeration
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 ...
...


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:
...
...
Working payloads
cURL
Automatically handles the following request headers:
HostandContent-Length
netcat
Explanation for each data parameter
action=Apply
Our intended flow (to call the
validate_lan_ipaddrfunction) is enclosed by a few IF statements:
The following conditions must be met:
a. skip_amd_check must not be defined in the data parameter
b. action must be one of the following values: "Apply", "tmUnblock", "hndUnblock"
Returns <!--Invalid Value!<br> --> if the else block is taken instead
lan_ipaddr=4
The value appears to specify the number of data parameters
Eg. value of 4 -> index up to
lan_ipaddr_3This is simply a speculation from what have been observed from the HTTP traffic when interacting with the web page
The following line represents the
validate_lan_ipaddrfunction in Ghidra:
Focus on the following code snippet (within our intended code flow)
&variables

The following line calls the get_cgi function on the first value in variables ("lan_ipaddr"):
This CGI parameter has to be specified in order for our code flow to reach our intended function
lan_netmask=x(can be empty)
We are now able to call the validate_lan_ipaddr function. The following code snippet shows the first few lines:
The CGI data parameter
lan_netmaskwill be checked for existenceReturn from function if it does not exist
Hence, we simply have to specify this parameter (even with an empty value) in order to pass the checks, and proceed to the
get_merge_ipaddrfunction
lan_ipaddr_0=lan0&lan_ipaddr_1=lan1&lan_ipaddr_2=lan2&lan_ipaddr_3=lan3
Range of values that are directly inserted to the stack without bounds checking = WIN!
Investigating buffer overflow
determine if SP value is constant at specific breakpoints in the program execution, across multiple executions
If so (likely) -> set breakpoint at the start of
apply_cgifunction to determine the stack address which registerrais written toUse that knowledge (and stack address we can write to) to subtract and find the offset needed to overwrite the return address
RCE
overwrite saved return address in the
get_merge_ipaddrfunction...
Last updated