Talos Vulnerability Report

TALOS-2025-2227

Planet WGR-500 swctrl OS command injection vulnerabilities

October 7, 2025
CVE Number

CVE-2025-54404,CVE-2025-54403

SUMMARY

Multiple OS command injection vulnerabilities exist in the swctrl functionality of Planet WGR-500 v1.3411b190912. A specially crafted network request can lead to arbitrary command execution. An attacker can send a network request to trigger these vulnerabilities.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Planet WGR-500 v1.3411b190912

PRODUCT URLS

WGR-500 - https://www.planet.com.tw/

CVSSv3 SCORE

8.8 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-78 - Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)

DETAILS

The WGR-500 is a high-performance industrial router designed to support VLAN configurations, equipped with a built-in firewall, and offering a robust suite of advanced networking functionalities.

The WGR-500 device features a service named swctrl that operates over UDP using a custom protocol. This protocol enables a sender to issue commands to the service. The initial portion of the function responsible for managing these requests is shown below:

void manage_request(int32_t socket) __noreturn

{
    char recv_buff[0x100];
    memset(&recv_buff, 0, 0x100);

    while (true)
    {
        int32_t addrlen = 0x10;
        memset(&recv_buff, 0, 0x100);
        struct sockaddr_in sockaddr;
        int32_t num_of_bytes_recv =
            recvfrom(socket, &recv_buff, 0x100, 0, &sockaddr, &addrlen);
        int32_t num_of_bytes_recv_1 = num_of_bytes_recv;

        if (num_of_bytes_recv >= 0)
        {
            struct packet_header header;
            struct packet_header* var_2c = &header;
            memcpy(&header, &recv_buff, 0x10);

 [1]        if (!strncmp(var_2c, "PLANETut", 8))

The manage_request function reads from the socket up to 0x100 bytes into the recv_buff buffer, that is of type packet_header:

struct packet_header __packed
{
    char magic[0x8];
    uint16_t command;
    char mac_addr_provided[0x6];
};

This packet_header structure contains:

  • magic: An 8-byte field that is compared at [1]. For execution to proceed, this field must contain the string “PLANETut”.
  • command: A uint16_t field, although only a uint8_t (the lower 8 bits) is used for the actual command.
  • mac_addr_provided: A 6-byte field intended for the MAC address of the device’s main interface.

If the received command is 0x90 (represented by CHANGE_SETTINGS_0x90), the swctrl service enters the “change settings” branch. This functionality allows for password modification and configuration of the device’s network settings (DHCP or static IP).

if (command_recv == CHANGE_SETTINGS_0x90)
{
    struct change_pwd_struct change_pwd_struct;
    memset(&change_pwd_struct, 0, 0x62);
[2] memcpy(&change_pwd_struct, &recv_buff, 0x62);

[3] if (!check_password(&change_pwd_struct.password))
    {
        [...]
    }
    else
    {
        uint32_t use_dhcp = change_pwd_struct.use_dhcp;

        if (change_pwd_struct.new_password[0])
        {
            uint8_t* new_pass_ptr = &change_pwd_struct.new_password;
            uint8_t next_new_pass_char = change_pwd_struct.new_password[0];

[4]         while (true)
            {
                uint8_t old_pass_char =
                    *(uint8_t*)(new_pass_ptr - 0x10);

                *new_pass_ptr = ( 
                    (uint8_t)(next_new_pass_char >> 4) & 0xf)
                    | (uint8_t)(next_new_pass_char << 4);

                *(uint8_t*)(new_pass_ptr - 0x10) = (
                    (uint8_t)(old_pass_char >> 4) & 0xf)
                    | (uint8_t)(old_pass_char << 4);

                new_pass_ptr = &new_pass_ptr[1];

                // end of the struct
                if (new_pass_ptr == (change_pwd_struct + 0x60))
                    break;

                next_new_pass_char = *new_pass_ptr;
            }

            char* new_pwd_ptr =
                &change_pwd_struct.new_password;
[5]         sprintf(&string_to_cmd, 
                "flash set USER_PASSWORD %s", 
                &change_pwd_struct.new_password);
[6]             system(&string_to_cmd);

The code above, at [2], loads 0x62 bytes of the received message into the change_pwd_struct variable, which is of type struct change_pwd_struct:

struct change_pwd_struct __packed
{
    struct packet_header packet_header;
    char use_dhcp;
    uint8_t field_11;
    uint8_t field_12;
    uint8_t field_13;
    uint8_t ip[0x4];
    uint8_t subnet[0x4];
    uint8_t gateway[0x4];
    char device_name[0x20];
    char password[0x10];
    char new_password[0x10];
    __offset(0x60)
};

This structure contains the current password under the password field. The provided password is checked at [3]. If the password is correct, the code at [4] is executed. Within the loop at [4], the password and new_password fields of the message are “decoded.” This decoding process involves swapping the nibbles (the first 4 bits and the last 4 bits) of each byte.

At [5], the string "flash set USER_PASSWORD <new_password>" is constructed and then used at [6] as a command for system to change the password.

Subsequently, the use_dhcp field in the provided packet is checked and used to adjust settings accordingly:

    uint32_t first_ip_octect = (uint32_t)change_pwd_struct.ip[0];
    if (use_dhcp)
        strcpy(&return_message_buff, "Set DHCP done.");
    else if (first_ip_octect == 127)
    {
        [... error branch ...]
    }
    else
    {
        [... set ip, netmask and gateway in the flash ...]

[7]     char* device_name_cursor =
            &change_pwd_struct.device_name;
        int32_t idx = 0;
        char* next_hostname_char =
            (uint32_t)change_pwd_struct.device_name[0];

        while (true)
        {
            device_name_cursor = &device_name_cursor[1];

            if (next_hostname_char != '\n' && next_hostname_char != ' '
                && next_hostname_char)
            {
                new_device_name[idx] =
                    (char)next_hostname_char;
                idx += 1;
            }

            if (device_name_cursor ==
                    &change_pwd_struct.password)
                break;

            next_hostname_char =
                (uint32_t)*(uint8_t*)device_name_cursor;
        }

  [8]   sprintf(&temp_stack_buff, 
            "flash set DEVICE_NAME %s", 
            &new_device_name);
  [9]   system(&temp_stack_buff);
        [...]
    }

If use_dhcp is not set and the first octet of the provided IP address is not 127, then the code at [7] is reached. The device_name_cursor variable is then parsed to skip newlines and spaces, and the resulting hostname is stored in new_device_name. At [8], the string "flash set DEVICE_NAME <new_device_name>" is created and subsequently used at [9] with the system function to modify the DEVICE_NAME variable in the device’s flash memory.

In both cases, the strings passed to system are constructed using attacker-controlled input without proper sanitization, allowing arbitrary shell command execution. Each instance is detailed below.

CVE-2025-54403 - new_password

The swctrl program allows, via command 0x90, the modification of the administrator password. At [5], the string "flash set USER_PASSWORD <new_password>" is constructed and subsequently used by the system function at [6]. This creates an OS command injection vulnerability at [6] through the new_password string received by swctrl. An attacker can exploit this vulnerability to execute arbitrary operating system commands.

CVE-2025-54404 - new_device_name

The swctrl program allows, via command 0x90, the modification of the device’s DEVICE_NAME. At [8], the string "flash set DEVICE_NAME <new_device_name>" is constructed and subsequently used by the system function at [9]. This creates an OS command injection vulnerability at [9] through the new_device_name string received by swctrl. An attacker can exploit this vulnerability to execute arbitrary operating system commands.

TIMELINE

2025-07-30 - Initial Vendor Contact
2025-08-01 - Vendor Disclosure
2025-08-01 - Vendor Confirmed Receipt
2025-09-01 - Status Update Request
2025-09-01 - Vendor Reply
2025-09-24 - Vendor Reply Acknowledged. Release Date Announced.
2025-10-07 - Public Release

Credit

Discovered by Francesco Benvenuto of Cisco Talos.