CVE-2025-60690
Overview of each section
Ghidra
Disassembly of the
get_merge_ipaddrfunction
apply_cgi+validate_lan_ipaddrsource code analysis + payload generation
Analysis of the source code to understand how we can reach the vulnerable function
Generation of the initial working payload to control values in the buffer
strcatfunction analysis + manual breakpoints
httpdbinary patching method to set a manual breakpointIdentification of the argument values passed to the vulnerable strcat function
Calculation of the offset between buffer and return address
Investigating buffer oveflow
Generation of payload to overwrite the return address and trigger a SIGSEGV
Investigating ret2 methods + ROP
Overview of basic ROP gadgets
Enumerate gadgets from
/lib/libc.so.0
Exploit development (METHOD 1: ROP + ret2libc)
Exploit development (METHOD 2: ROP to shell-code)
1. Ghidra
1.1 get_merge_ipaddr
get_merge_ipaddr
1.2 Refined get_merge_ipaddr
get_merge_ipaddr1.3 Analysis
Keep in mind that we are working with the MIPS assembly for this binary. The instructions 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
1.3.1 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)
1.3.2 Important notes from each section (Ghidra)
Disassembly
The
a0toaXregisters 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 the current functionThe source of truth of argument passing should come from the
a0,a1,an, etc. registers in the disassembly, rather than theparam_Xvariable naming in decompiled code
2. apply_cgi source code analysis + initial HTTP request generation
apply_cgi source code analysis + initial HTTP request generationIn this section, I will present the following:
Working HTTP POST request that will allow us to reach the vulnerable portion of the code
The explanation of each data parameters used
2.1 Working request format 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
2.2 Explanation of 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=x
The value provided to this parameter does not require any significant values, as it is not provided as an argument to the final
get_merge_ipaddrfunctionThe following line represents the
validate_lan_ipaddrfunction call in Ghidra:
Focus on the following code snippet (within our intended code flow)
Notice that the
ppuVar5variable is assigned to the pointer tovariables
&variables

The following line calls the get_cgi function on the first value in &variables ("lan_ipaddr"):
Thus, the lan_ipaddr CGI parameter has to be specified in order for our code flow to reach our intended function
After we have passed the first IF statement:
We have to find a way to pass the second IF statement, to eventually reach our intended validate_lan_ipaddr function call:
We can simply set pcVar3 to a non-empty string (!= '\0') to pass the following checks:
Thus, any value provided to this field such as: lan_ipaddr=x will work
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. Let's refer back to a code snippet from the (refined) get_merge_ipaddr function:
We are able to gather the following information:
snprintf function concats the string values with the specifier format:
a0_ito the acStack_48 variablethe
a0variable is passed in as the 1st argument toget_merge_ipaddr, while the variableiiterates from 0 to 3the concatenated string will be used as a CGI parameter key to retrieve a value, and store it into the
__srcvariablethe
__srcvariable will than be concatenated into the address pointed to bya1(2nd argument toget_merge_ipaddr) in the vulnerable strcat function
How are we able to identify the parameter name (lan_ipaddr_n=X, eg. lan_ipaddr_0=X)?
a. lan_ipaddr=X parameter (from previous findings)
b. Analysis of HTTP POST request generated by web form:
c. Analysis of argument values passed to the strcat function using gdb+gdbserver setup
3. strcat function analysis + manual breakpoints
strcat function analysis + manual breakpointsRefer to the GDB, gdbserver section under the Expoit research page, for more information on the setup process
Refer to the following disassembly snippet:
Disassembly (Ghidra):

Disassembly (gdb):
In order to identify the relevant values, we can set a breakpoint@0x0041f99c, and print the values of the following registers:
raat address$sp+96a0andv0to identify the 1st and 2nd argument to the strcat function
Notice that the value in the
v0register is moved to thea1register in the delay slotAs we are not able to reliably predict when the delay slot on line 0x41f99c will run, we can set a breakpoint before the strcat function call (0x0041f99c), and retrieve the value directly from the
v0register instead
s2to identify the CGI parameter name
3.1 Set custom breakpoint (patch method)
3.1.1 Patch command
of=/tmp/httpd: output filebs=1: byte-size set to 1seek: offset from the httpd base (0x00400000). Refer to the section
3.1.2 Run the patched binary (device) + attach to gdbserver
3.2 Analysis of register values (a0, v0,s2)
a0, v0,s2)Next, we can send a HTTP POST request to the httpd web server using what we have discovered from previous findings:
Notice the parameter value of lan0 to the
lan_ipaddr_0key
From a separate terminal, send a manual SIGSTOP signal to the httpd process:
Now, we can view the values:

Take note of the following:
The return address for the current function (get_merge_ipaddr:
$sp+96 = 0x7ffa4e68) is at a lower memory address than the buffer we are able to write from the strcat function (p/x $a0 = 0x7ffa4e88)
This presents us with an issue when we are attempting to control the
raregisterRefer to the next section for more information on this issue
Since the delay slot (line 0x41f99c) is not taken as a result of our custom breakpoint, the $a1 register will not contain the actual value to the strcat function
Instead, we can view the expected content of the $a1 register via the $v0 register, which will contain the value we supplied to the lan_ipaddr_0 parameter in our HTTP POST request (eg. "lan0" in our example)
note that the $v0 register contains the return value from the previous function that was called, which happens to be get_cgi
We can also view the name of the CGI parameter that we can control by reading the value in the $s2 register
the value of this CGI parameter is directly concatenated to the buffer
with our breakpoint example, the value will be "lan_ipaddr_0"
4 Investigating buffer overflow (get_merge_ip_addr function)
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
4.1 Stack layout constraints
Normally, we would want to overwrite the return address of the current function (get_merge_ipaddr). However, the buffer variable is passed in as an argument from the calling function (validate_lan_ipaddr), resulting buffer address to be placed higher than the return address of the current function:
The buffer address is higher than the stack portion that is used to restore to s0-s7 and ra
In the MIPS architecture, values are written towards higher memory address. Thus, this means that the stack layout physically prevents us from overwriting the return address of the current (
get_merge_ipaddr) function
To bypass this issue, we attempt to overwrite stack portions (that restores to ra, s0-s7) of the calling function instead (validate_lan_ipaddr).
Refer to the following disassembly code snippet from the end of the validate_lan_ipaddr function:

We can understand the following:
Line 0x4285f4: The offset value $sp+132 is used to restore the $ra register
Let's perform a few calculations to calculate the offset values:
Stack values/offsets calculation:
a. Buffer ($a0) - $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
⇒ 132 is from the lw ra, 132(sp) instruction we have found before
c. Offset between buffer and ra (validate_lan_ipaddr): 0xec - 0x80 = 0x6c = 108 (decimal)
4.2 Working payload to overwrite & control ra register value (in validate_lan_ipaddr function)
ra register value (in validate_lan_ipaddr function)Take note of the following:
The offset padding value used in the payload is 102, instead of 108 in the payload:
"A" * 102We will find out the reason for this in the explanation below
The values supplied to the
lan_ipaddr_XCGI parametersa,b,c,python3 -c ...
1. Testing overwrite of the ra register (validate_lan_ipaddr)
ra register (validate_lan_ipaddr)For this section, assume a break point is set on the return (0x00428614) line in the validate_lan_ipaddr function:
of=/tmp/httpd: output filebs=1: byte-size set to 1seek: offset from the httpd base (0x00400000). Refer to the section
After the POST request is sent, we are able to use the offset $sp-0x68+0x80 to locate the buffer and the subsequent overwritten values on the stack
The 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 us 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 payload willl accommodatesfor the length of the merged string ("a.b.c."), by decrementing the padding value by 6 ⇒ 108 - 6 = 102
"XXXX" will be stored to the ra register
We are also able to control the
s0tos6register values (note: "X"=0xA):

2. 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
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
/lib/ld-uClibc.so.0/usr/lib/libnvram.so/usr/lib/libshared.so/usr/lib/libpolarssl.so/usr/lib/libexpat.so/lib/libgcc_s.so.1/lib/libc.so.0
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 Exploit 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):
ROP + ret2libc:
ROP: utilize gadgets to load
a0register with string argument to executeret2libc: directly call the
systemfunction (libc), with a controlled argument (from ROP)
ROP to shell-code
series of 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.
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:
We can use the general search command:
5.3.1 Method 1: ROP + ret2libc
The 1st method (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)
Gadget 1: Increment value of $sp, to allow string argument ($a0) to appear at the end of payload
Recall that the $a0 register 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
For this to happen, we need to increment the $sp to allow future gadgets to be able to use a higher value $sp to locate the string argument (to write to $a0)
As we are looking for an instruction pattern that increments the stack-pointer, it will likely appear at the end of a function during the return statement (as a delay slot). Thus, we have to include the search for a jump instruction before our $sp increment instruction (likely instruction
jr $ra)Expected patterns to match:

Important instructions:
lw ra,72(sp): Load the word (4 bytes) value from $sp+72 to $ra
Allows us to control the memory address that is written to $ra
To jump to next gadget
jr ra: Jump to address stored in $raaddiu sp,sp,80: Delay slot that increments $sp by 80
Gadget 2: Load known stack offset value into register $a0 , and subsequently jump to the system function
Expected pattern to match:

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, which constitutes a bad character
Important instructions:
addiu a0,sp,24: increment stack-pointer by 24, and move the calculated value into the $a0 registermove t9,s0: move the value of $s0 into $t9
Recall that we are able to control the value of the $s0 register from the return from the validate_lan_ipaddr function
This will aid us in calling the final system function
jalr t9: jump to address in $t9
5.3.2 Method 2: ROP to shell-code (PENDING UPDATE)
We attempt to look for ROP gadgets within the lib.so.0 binary
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:
...
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:
...
6. Exploit development (METHOD 1: ROP + ret2libc)
6.1 Finding address of system function
system functionRecall from previous findings that our device employs partial ASLR that does not randomize the addresses in our system library functions (libc). Thus, we will be able to find the fixed and predictable address of the system function
Retrieve 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
The relative offset value of the system function from start of libc is 0x29ea0
6.2 Crafting the payload
Let's recall information that we have gathered:
ROP gadgets
libc system function address
0x2ad61ea0
6.2.1 Important considerations
a. Stack frame size of each functions 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
Characters that must not be included in the payload
eg. gadget memory address, string arguments, etc.
As we are inserting into the buffer via the strcat function, the only bad character will most likely only be the null terminator value (
0x00)
6.2.2 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. We will also need to account for the frame size of both the validate_lan_ipaddr and get_merge_ipaddr (0x68 and 0x88)
The
addiufunction at the end of thevalidate_lan_ipaddrfunction will execute as a delay slot following the jump (jr ra) to the 1st gadget
The following presents the calculated offset values from the buffer to reach each respective payload values:
a. 0x6c: return address of the validate_lan_ipaddr function ⇒ address of gadget 1 (ra)
b. -0x80+0x68+0x88+0x48 = 0xb8 ⇒ address of gadget 2
value of 0x48 is taken gadget 1
c. -0x80+0x68+0x88+0x50+0x18 = 0xd8 ⇒ string argument to the system function (a0)
value of 0x50 taken from gadget 1
value of 0x18 taken from gadget 2
value of 0x88 taken from delay slot after jump at the end of validate_lan_ipaddr
d. 0x6c-28 = 0x6c-0x1c = 0x50 ⇒ address of the system function (s0)
Refer to the folloiwng diagram for an illustration of the stack layout, along with the relevant payload values:

NOTE: we have to account for 6 characterss:
x.x.x.from the first 3 lan_ipaddr parameters
(1) General payload format
(2) Ping attacker machine
(3) Reverse shell
The shell commands will be explained in later sections
cURL
Breakpoint analysis at respective locations
We will attempt to set breakpoints at specified locations, to aid us in the analysis of relevant portions of the stack and to verify the flow of the exploit:
Return line in validate_lan_ipaddr (
0x428614)
address in
ra->0x2ad6a044(gadget 1)address in
s0->0x2ad61ea0(system)Instructions at each of the gadget addresses should be what we expect
address in
$sp+0x88+72-> gadget 2 (0x2ad4d8fc)value of
0x88from delay slot at the end of validate_lan_ipaddrvalue of
72from increment value used in gadget 1



Start of system function (
0x2ad61ea0)
string value in
$a0-> shell command to execute
Recall that the system offset address from libc base is 0x29ea0
Remember that the system function is found within the libc library. Thus, we have to patch the break instructions in /lib/libc.so.0 itself, and update the httpd binary to use it:
Next, we have to update the library path:


Additional steps to make the exploit more "stealthy"
Notice that our current exploit will crash the httpd web server. This is because the final system function that is called in our ROP+ret2libc chain attempts to load a memory address value from a certain offset from the stack to update the ra register, and subsequently execute the instructions at the memory address
However, this address is likely to be a padding value used in our payload (bunch of As), which will trigger a SIGSEGV (Segmentation Fault), and simply crash
To better understand this, lets take a look at the last few instructions in the system (0x2ad61ea0) function:
We can ignore the rest of the lw instructions that loads to the other registers, and focus on the following 2 lines:
We now know that the relative stack offset of 64 (0x40) is used to load the ra register, which is subsequently used in the jump instruction
Thus, we could attempt to extend our payload to add a few additional gadgets to place our next memory address (likely somewhere in the original get_merge_ipaddr function).
Alternatively, we can employ a simple method to add an additional command in our payload to restore the httpd process. Refer to the post-exploitation section for more details
7. Exploit development (METHOD 2: ROP to shell-code)
7.1 Understanding payload layout on the stack

...
7.2 Building the exploit
7.2.1 Custom "ping" code
7.2.2 Explanation of the custom "ping" code:
.set noreoder
...
7.3 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
There exists 2 different caches, namely the Instruction Cache (I-Cache) and Data Cache (D-Cache), that stores instructions and data respectively. This may cause mismatches between data written by us to the stack (subsequently stored to the cache), and the instruction we want the CPU to execute
Thus, when the CPU attempts to access the shellcode instructions at the specified memory addresses from the I-Cache, it may retrieve "stale" data (old data from stack frames of previous function calls) which may cause a crash when executed
2.1 Overview of I-Cache and D-Cache:
I-Cache: Dynamic buffer of whatever value (from a particular memory address) that the CPU was recently told to execute
It operates on the assumption that code is static and does not monitor memory writes
D-Cache: Dynamic buffer of what have been written and retrieved from the stack
this is where the buffer overflow payload (shellcode) initially resides after being written to the stack
2.2 Overview of steps to defeat "cache incoherency" issues
Let's assume that that the "write-back" cache policy is used:
data is written only to the D-Cache and marked as "dirty"
it only reaches the RAM (actual stack memory) when the cache line is evicted or manually flushed
Possible scenario:
I-Cache currently stores the instruction of the httpd binary that was loaded at the start of the execution
Execute
sleep()function
A few context switches will happen during the sleep period
When the OS switches away from the
httpdprocess to another process, the current data needs to be saved:
the kernel may perform a D-Cache clean, which takes all the "dirty" line on the D-Cache (including shellcode) and pushes them to the physical RAM
When the OS switches back to the
httpdprocess, the I-Cache will need to be invalidated (to prevent leak of instructions between processes)
the I-Cache will be marked as invalid
When the PC attempts to execute the shellcode (final portion of the ROP chain), the CPU will look in the I-Cache
an I-Cache miss will happen, since it was previously marked as invalid
the CPU will look for the required instructions from the physical RAM, which will pull our shellcode into the I-Cache
...
Last updated
