CVE-2025-60690

1. Ghidra

get_merge_ipaddr

Refined 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. (#1) Move a1 to s1

  • Moves the value of the 2nd argument to the get_merge_ipaddr function to the s1 register

  1. (#2) Move s1 to a0

  • a0 (value from s1) indicates the 1st argument to the following function call (strcat)

  1. (#3) Calls the strcat function (address stored in temporary register t9)

Important notes from each section

  1. Disassembly

  • The a0 to an registers indicates the exact values passed to each of the nested function calls

  1. Decompiled code

  • The param_1 and param_2 variables indicates the 1st and 2nd argument to the get_merge_ipaddr functions

  • However, even though the 2nd argument (param_2) to the get_merge_ipaddr function is shown to be passed as the 1st argument to the strcat function, this is not actually the case

    • param_2 refers to a1, which instead relates to the 2nd argument passed to the nested strcat function 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

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:

  1. ra can be found at address $sp+96 as seen from instruction 41f900

  2. s1 @ right before 41f994

  • The value stored in s1 is written to the a0 register (1st argument to strcat)

Calculate the distance between address stored in s1 and location where ra is saved:

3. Further enumeration

3.1 Ghidra

  1. Window -> Function call graph

  2. Window -> Function call tree

  3. Window -> Defined strings

    1. Right-click -> Data -> ... eg. TerminatedCString

    2. Right-click -> References -> show TerminatedCstring

  4. Right-click -> References -> show references to ...

  5. ...

3.2 Fuzzing

From UART console

  • Listen with gdbserver

  • It appears that sending a VALID POST request to /apply.cgi triggers 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_cgi function (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: Host and Content-Length

  • netcat

Explanation for each data parameter

  1. action=Apply

  • Our intended flow (to call the validate_lan_ipaddr function) 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

  1. lan_ipaddr=4

  • The value appears to specify the number of data parameters

    • Eg. value of 4 -> index up to lan_ipaddr_3

    • This 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_ipaddr function 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

  1. 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_netmask will be checked for existence

    • Return 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_ipaddr function

  1. 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_cgi function to determine the stack address which register ra is written to

    • Use 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_ipaddr function

  • ...

Last updated