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:
...
...
4. Source code analysis + payload generation
4.1 Working payload to write to buffer
Note: The basic token value YWRtaW46YWRtaW4= is the base64-encoded string of the value admin:admin
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!
4.2 Investigating buffer overflow (get_merge_ip_addr function)
The value of $sp is in context of the
get_merge_ipaddrfunctionThe value ra refers to the stack portion that is restored to the ra register
Normally, we would want to overwrite the return address of the current function. However, in our case, we have encountered the following issue:
Buffer address is higher than the stack portion that is used to restore to s0-s7 and ra -> physically impossible to overwrite the address (values are written to higher memory addresses)
To bypass this issue, we attempt to overwrite stack portions (that restores to ra, s0-s7) of the calling function instead (
validate_lan_ipaddr)Useful stack values/offsets discovered:
a. Buffer - $sp = 0x80 ⇒ buffer = $sp + 0x80
b. Stack address portion restored to ra in validate_lan_ipaddr: $sp+0x68+132 = $sp + 0xec
=> $sp+0x68 happens when merge_ip_addr function returns
c. Offset between buffer and ra (validate_lan_ipaddr): 0xec - 0x80 = 0x6c = 108 (decimal)
4.3 Working payload to overwrite & control ra register value (in validate_ip_addr function)
Testing overwrite of ra register
For this section, assume a break point is set on the final return line in the validate_lan_ipaddr function:
After the POST request is sent, we are able to use the offset
$sp-0x68+0x80to locate the buffer and the subsequent overwritten values on the stackThe value
0x68represents the frame size (that is used to increment the $sp at the end of theget_merge_ipaddrfunction right before it returns). Thus, it is subtracted from the current $sp to bring it back to the context of theget_merge_ipaddrfunctionThe value
0x80is the relative offset between the buffer and $sp
Notice the custom payload value passed to the lan_ipaddr_3 parameter:
We have identified previously that the offset between the buffer and ra (
validate_lan_ipaddr) is 0x6c = 108 (decimal)From the stack value shown in the memory analysis, we can see that buffer contains the value "a.b.c." (length 6) and subsequent 'A's
this is because the first 3 lan_ipaddr_X parameters (lan_ipaddr_0 to lan_ipaddr2) has the value "a", "b" and "c" respectively, which are merged with a dot (.)
Thus, the python3 payload simply accommodates for the length of the merged string ("a.b.c.")
⇒ 6 + 102 = 108
"XXXX" will be stored to the ra register
We are also able to control the
s0tos6register values (note: "X"=0xA):

Forcing SIGSEGV (segmentation fault)
Now, we can attempt to execute the request. on the binary without any break points
A SIGSEGV error is encountered immediately after the request is sent:

Notice the error message:
0x58585858-> "XXXX", which is the value we have written
HOORAY, this tells us that we are able to successfully control the return address!
5. Investigating ret2 methods + ROP/JOP
5.1 Search locations
Main binary (
httpd)
We will not search for gadgets within the main binary as its address space only consists of 3 bytes
This means that there will be
0x00for the 1st byte (eg.0x00123456), which represents a null byte, and constitutes a bad character when working with our payload
Loaded libraries
libc.solibuClibclibshared.solibnvram.so
5.2 Common search patterns
Load from stack to registers
a0andt9(or evenra) followed by jump tot9
Take note that some of the gadgets may have delay slots, which will execute BEFORE the jump instruction
This gadget allows us to directly call a function at a specified address (eg.
libc), with control of the first argument (relative stack position)
Load from stack position (relative to the stack pointer) to a register (eg.
s0)
Load value from a predefined offset (eg. 96) from the stack into the
s0registerthe value in the resulting register
s0can for example, store a controlled memory address.this address can be directly moved into a register (eg.
t9), before a subsequent jump is executed (refer to next gadget)
Load from register (we can control) to
t9, followed by jump tot9
This gadget allows us to jump to custom memory address values stored in known registers (eg.
s0)
Load predictable memory address values (offset from known stack pointer value) to a register (eg.
s0)
5.3 Search methods
Before we continue, I would like to discuss 2 possible methods that we can use to achieve our final goal of Remote-Code Execution (RCE):
JOP/ROP + ret2libc:
JOP/ROP: utilize gadgets to load
a0register with string argument to executeret2libc: directly call the
systemfunction (libc), with a controlled argument (from JOP/ROP)
JOP/ROP to shell-code
series of JOP/ROP gadgets to load relevant register values
end goal will be to eventually call a shell-code stored on a known offset on the stack
shell-code can contain a call to relevant
libclibraries, etc.
5.3.1 Method 1: JOP/ROP + ret2libc
The 1st method (JOP/ROP + ret2libc) will be preferred over the 2nd, as it more straightforward, and does not have to deal with some known issues: of shell-code encoding, cache incoherency, etc. (discussed below)
Tools: ROPGadget, manual search (disassembly)
Other possible tool(s):
ropper
First, lets run the ROPGadget tool on the
/lib/libc.so.0binary, and save the output to a file:
Next, we can perform a search to look for the following pattern that aims to provide all the necessary functions in a single gadget:
due to the delay slot, the
addiuinstruction is expected to appear after thejalr
Command:
The
-Poption to grep allows us to specify a Perl regex pattern that provides the following features:Allow arbitrary number of instructions in between our searched instructions
Handles newlines

From the results, we can see that we are able to find a back-to-back jalr and addiu instructions. However, the next closest instruction that modifies the $t9 register (preceding the 2 instructions mentioned above), deals with the $gp register, which is unreliable to overwrite
Thus, we have to find a solution that employs a JOP/ROP gadgets chain that gradually loads specific known register values, and works towards the final aim of being able to control the final a0 register and jump to a specified libc function
...
[....attempt to search in libc]
Gadget 1: Load zero value into a known register

Gadget 2: Load zero from known register to controlled offset on stack
...
Gadget 1: Increment value of $sp, to allow string argument ($a0) to appear at the end of payload
Expected pattern to match:

...
Gadget 2: Load known stack offset value into register $a0
Expected pattern to match:
We will attempt to find a high stack offset value (end of payload) to control the
$a0registerRecall that the
$a0register will store our string argument to the system function. Thus, we need to put the string at the end of our payload, to prevent the null byte (bad character) from interfering


Note that we will jump to address 0x158fc instead of 0x15900, as 0x15900 will result in a null character (
0x00) when added with libc base
Important instructions:
a. ...
Gadget : ...
Expected pattern to match:

Important instructions:
a.
..
...
5.3.2 Method 2: JOP/ROP to shell-code
We attempt to look for JOP/ROP gadgets within the httpd binary. Given that we are able to find appropriate gadgets, we can entirely skip the need to even search within the other libraries such as libc
Gadget 1: addiu from stack offset to a controlled register, jump to 2nd gadget (from controlled register)
Perl regex for pattern (grep
-Pflag):addiu $sX, $sp, 0xAAAA

Starting address of found code portion: 0x00481d1c (
get_device_settings)

Important instructions:
a. addiu s1,sp,24
Stores the calculated value of sp+24 into the register
s1Since we are able to control this portion of the stack, we can control what is stored into
s1, and eventually the jump address in the later steps
b. move t9,s3
Move register
s3intot9
c. jalr t9
Jump to instruction in
t9We know from previous analysis that we are also able to control register
s3. Thus, this allows us to control the jump to gadget 2
Gadget 2: move from a controlled register to register t9, followed by a jump to t9
Perl regex for pattern (grep
-Pflag):move $t9, $sX ;
Starting address: 0x00474b44 (
set_wlan_radio_security)

Important instructions:
a. move t9, s1
From gadget 1, we are able to control the value at a calculated stack offset, where this particular stack address is stored in register
s1Thus, we are able to control the value in
t9
b. jalr t9
Jump to
t9, which stores our custom shell-code instructions
6. Exploit development (METHOD 1: JOP/ROP + ret2libc)
6.1 Finding address of system function
system functionRetrieve absolute
systemfunction address
Retrieve relative address offset from start of libc
For this approach, we have to manually calculate the absolute
systemaddress by adding together the start address of libc and the offset value retrieved
6.2 Crafting the payload
Let's take a look at the information that we have gathered:
ROP/JOP gadgets
libc system function address
0x2ad61ea0
Important considerations
a. Stack frame size of each function involved
get_merge_ipaddr:
0x68validate_lan_ipaddr:
0x88
b. Function context in which each gadget will be executing (crucial for relative stack offset calculation
Gadget 1's execution context:
validate_lan_ipaddrfunctionGadget 2's execution context:
validate_lan_ipaddrfunction
c. Bad characters (0x20, 0x00, 0x3a, 0x0a, 0x3f)
Characters that must not be included in the payload
eg. gadget memory address, string arguments, etc.
Relative offset values from the buffer (to overwrite)
Before we continue, let's recall the following values:
Offset between buffer and sp:
0x80
Notice that both of the gadget's execution context is in the validate_lan_ipaddr function. Thus, we will only need to account for the frame size of the get_merge_ipaddr (positive offset value of 0x68 from the sp)
the frame size from the
validate_lan_ipaddrfunction will not need to be added since the particularaddiuinstruction will not be reached before the jump to our 1st gadget
a. 0x6c: return address of the get_merge_ipaddr function -> address of gadget 1 (ra)
b. -0x80+0x68+0x88+0x48 = 0xb8: address of gadget 2
c. -0x80+0x68+0x88+0x50+0x18 = 0xd8: string argument to the system function (a0)
value of 0x50 from gadget 1
value of 0x18 from gadget 2
take note of the frame size of the functions involved
0x88from delay slot after jump at the end ofvalidate_lan_ipaddr
d. 0x6c-28 = 0x6c-0x1c = 0x50: address of the system function (s0)

...
~~~~
NOTE: we have to account for 6 characterss:
x.x.x.from first 3 lan_ipaddr parameters~~~~
(1) Python3 payload (ping attacker machine)
(2) Python3 payload (Netcat reverse shell)
Preparations on the attacker machine:
the
busybox-mipselbinary can be found here
Breakdown of the payload commands:
$0arepresents the line feed (newline) character in URL-encoding. This can be used to separate the commands (in shell).Note that We can also use the shell operators
;,&&or|too, but it may not be as reliable, as it may be filtered by the web server
...
cURL
Break-point analysis at respective locations
We will attempt to set break-points at specified locations, and analyse relevant portions of the stack, to verify the flow of the exploit:
Return line in
validate_lan_ipaddr(jr ra)
address in
ra->0x2ad6a044(gadget 1)address in
s0->0x2ad4d8fc(system)Instructions at each of the gadget addresses should be what we expect
address in $sp+72 -> gadget 2 (
0x2ad4d8fc)
Start of system function (
0x2ad61ea0)
address in $sp+80+24 -> string argument to system function
...


Set breakpoint within libc
7. Exploit development (METHOD 2: JOP/ROP to shell-code)
7.1 Understanding payload layout on the stack

...
7.2 Building the exploit
7.2.1 Custom "ping" code
Explanation of the custom "ping" code:
.set noreoder
...
Possible issues to look out for
Encoding: certain instructions may contain characters that may signify a null byte or similar type of byte that terminates the string early
eg.
.asciz,nop, etc.in the case of the
strcatfunction, this may cause the final payload to be truncated when written to the stack
Cache incoherency
...
Last updated
