Talos Vulnerability Report

TALOS-2024-2037

Wavlink AC3000 fw_check.sh Firmware Upload vulnerability

January 14, 2025
CVE Number

CVE-2024-39273

SUMMARY

A firmware update vulnerability exists in the fw_check.sh functionality of Wavlink AC3000 M33A8.V5030.210505. A specially crafted HTTP request can lead to arbitrary firmware update. An attacker can perform a man-in-the-middle attack to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

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

Wavlink AC3000 M33A8.V5030.210505

PRODUCT URLS

Wavlink AC3000 - https://www.wavlink.com/en_us/product/WL-WN533A8.html

CVSSv3 SCORE

9.0 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-306 - Missing Authentication for Critical Function

DETAILS

The Wavlink AC3000 wireless router is predominately one of the most popular gigabit routers in the US, in part due to both its potential wireless and wired speed capabilities and extremely low price point (costing at the time of this writing ~$60 USD). Among the configuration options, it’s also able to act as a standalone wireless gateway, a basic network router, or a wireless repeater.

As part of the updating functionality of the Wavlink AC3000, there exists the sbin/fw_check.sh script which is used to check for firmware updates. After NTP is setup correctly (which occurs immediately after the WAN side is connected) the sbin/schedule.sh script adds fw_check.sh to the crontab as such:

if [ "$mesh" = "2" ]; then
    echo "0 4 * * * fw_check.sh" >> /var/spool/cron/crontabs/"$user"
else
    echo "30 4 * * * fw_check.sh" >> /var/spool/cron/crontabs/"$user"
fi

Continuing within fw_check.sh:

url1=`nvram_get 2860 FW_CheckLink1`  // [1]
url2=`nvram_get 2860 FW_CheckLink2`  // [2]

if [ "$url1" = "" ]; then
    echo "url1 is null"
    if [ "$url2" = "" ]; then
        echo "url2 is null"
        echo 0 > /tmp/update &
        exit 0
    fi
fi

echo 1 > /tmp/update &
curl -k -I -m 5 -o /dev/null -s -w %{http_code} "$url1" > /tmp/url1  // [3]
curl -k -I -m 5 -o /dev/null -s -w %{http_code} "$url2" > /tmp/url2  // [4]

#sleep 6

result=`cat /tmp/url1`

if [ "$result" != "200" ]; then
    echo "url1 result:$result"
    result=`cat /tmp/url2`
    if [ "$result" != "200" ]; then
        echo "url2 result:$result"
        echo 0 > /tmp/update &
        exit 0
    else
        echo "get js from $url2"
        curl -s -k "$url2" > /tmp/fw.tmp  // [5]
    fi
else
    echo "get js from $url1"
    curl -s -k "$url1" > /tmp/fw.tmp // [6]
fi

To summarize, at [1] and [2], two URLs are grabbed from the nvram and read into bash variables. These URLS are by default as follows:

FW_CheckLink1=http://fw.iqs.link/firmware/router/WN533A8-WAVLINK-WO.js
FW_CheckLink2=http://fw.wavlink.com/firmware/router/WN533A8-WAVLINK-WO.js

Very important to note that these are both HTTP and not HTTPS, as that will allow us to actually perform a Man-In-The-Middle attack here. Continuing, at [3] and [4], both of these URLS are curled and the HTTP codes retrieved are stored into files. Assuming that either URL has a valid HTTP 200 return code, the contents of that web page are then stored into the /tmp/fw.tmp file at [5] or [6]. Continuing on in fw_check.sh:

fw_url=`cat /tmp/fw.tmp |grep FW_URL |sed 's/^.*="//g'|sed 's/".*$//g'`
fw_md5=`cat /tmp/fw.tmp |grep FW_MD5 |sed 's/^.*="//g'|sed 's/".*$//g'`
fw_update=`cat /tmp/fw.tmp |grep FW_UPDATE |sed 's/^.*="//g'|sed 's/".*$//g'`
FW_VER=`cat /tmp/fw.tmp |grep FW_VER |sed 's/^.*="//g'|sed 's/".*$//g'`
fw_version=`cat /tmp/fw.tmp |grep FW_VER |sed 's/^.*="//g'|sed 's/\..*$//g'`
now_version=`web 2860 sys sdkVersion |sed 's/\..*$//g'`
now_date=`web 2860 sys sdkVersion |sed 's/^.*\.//g'`

curl -k -I -m 5 -o /dev/null -s -w %{http_code} "$fw_url" > /tmp/url_firmware

if [ "$now_version" = "$fw_version" ]; then
    echo "the right fw version for $fw_version"
    if [ "$now_date" -lt "$fw_update" ]; then
        echo 3 > /tmp/update &
#       sleep 6

        get_url=`cat /tmp/url_firmware`
        if [ "$get_url" = "200" ]; then
            echo "down load firmware from $fw_url"
            curl -s -k "$fw_url" > /var/tmpFW           // [5]
            echo 4 > /tmp/update &

The script expects the webpage to have something of a format like var FW_URL="<...>";, at which point it takes that value and curls it into the /var/tmpFW file at [5] . Continuing in the same script:

md5=`openssl dgst -sha256 /var/tmpFW | sed 's/^.*= //g'`
size=`wc -c /var/tmpFW | sed -re 's/[^0-9]*([0-9]*).*$/\1/;'`

if [ "$md5" = "$fw_md5" ]; then               // [6]
    echo "update firmware $FW_VER $size"
    killall -SIGHUP monitor&
    gpio l 16 2 2 4000 0 4000
    /bin/mtd_write -o 0 -l "$size" write /var/tmpFW Kernel  // [7]
    echo 5 > /tmp/update &
    sleep 10
    reboot
else
    echo 0 > /tmp/update &
    rm /tmp/fw.tmp &
    rm /var/tmpFW &
    echo "md5 is different, fw:$fw_md5, md5sum:$md5"
fi

Assuming that the sha256 sum of the firmware file matches that provided by the FW_MD5 variable listed in the curled web page from before, then we enter the branch at [6] and immediately flash this firmware with no further validation at [7]. Thus, if an attacker can perform a Man-In-The-Middle attack on the original HTTP request of fw_check.sh, they can flash arbitrary firmware onto the device.

TIMELINE

2024-07-25 - Initial Vendor Contact
2024-07-29 - Requesting reply from vendor
2024-07-30 - Vendor confirms receipt
2024-07-30 - Vendor Disclosure
2024-07-30 - Vendor confirms receipt
2024-09-02 - Status update request sent
2024-10-15 - Status update request. Upcoming expiration date announced.
2024-10-22 - Vendor replies product has been discontinued, but patches are being worked on
2024-11-04 - Status update request for patch release dates
2024-11-12 TALOS advisory release date announced
2025-01-14 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.