CVE-2025-60690

6.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

6.2 gdb, gdbserver

6.2.1 Compile gdbserver for the target's architecture

To 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

Next, we can run gdbserver on the target router:

We can use the steps outlined in the previous steps to transfer the gdbserver binary 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 machine

    • Since we know that our router uses the mipsel architecture (simply mips with 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:

  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:

6.3 Further enumeration

6.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. ...

6.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:

...

Last updated