Original text by Vincent Lee
Back In February, Ubiquiti released a new firmware update for the Ubiquiti EdgeRouter, fixing CVE-2021-22909/ZDI-21-601. The vulnerability lies in the firmware update procedure and allows a man-in-the-middle (MiTM) attacker to execute code as root on the device by serving a malicious firmware image when the system performs an automatic firmware update. The vulnerability was discovered and reported to the ZDI program by the researcher known as awxylitol.
This vulnerability may sound contrived; a bad actor gives bad firmware to the device and bad things happen. However, insecure download vulnerabilities have been the backbone of multiple Pwn2Own winning entries in the router category since its inception. The impact of this vulnerability is quite nuanced and worthy of further discussion.
How exactly does the router perform a firmware update?
According to Ubiquiti documentation, the new templated operational command
The templating system used by the Ubiquiti EdgeRouter is provided by the vyatta-op package. The command
$ cat node.def | |
help: Add a new image to the system | |
run: sudo /usr/sbin/ubnt-fw-latest —upgrade |
view rawCVE-2021-22909-snippet-1.console hosted with ❤ by GitHub
By running this operational command, the user is effectively invoking the
#!/bin/bash | |
#——————————————————————————- | |
STATUS_FILE=»/var/run/fw-latest-status» | |
UPGRADING_FILE=»/var/run/upgrading» | |
REBOOT_NEEDED_FILE=»/var/run/needsareboot» | |
DOWNLOADING_FILE=»/var/run/downloading» | |
URL=»https://fw-update.ubnt.com/api/firmware-latest» | |
ACTION=»refresh» | |
CHANNEL=»release» | |
DEFAULT_URL=»https://localhost/eat/my/shorts.tar» | |
#——————————————————————————- | |
while [[ $# -gt 0 ]] | |
do | |
key=»$1″ | |
case $key in | |
-r|—refresh) # Refresh status of latest firmware by | |
ACTION=»refresh» # fetching it from fw-update.ubnt.com | |
shift | |
;; | |
-s|—status) # Read latest firmware status from cache | |
ACTION=»status» | |
shift | |
;; | |
-u|—upgrade) # Upgrade to latest firmware | |
ACTION=»upgrade» | |
shift | |
;; | |
-c|—channel) # Target channel (release or public-beta) | |
CHANNEL=»$2″ | |
shift | |
shift | |
;; | |
*) # Ignore unknown arguments | |
shift | |
;; | |
esac | |
done | |
# … | |
upgrade_firmware() { | |
# Fetch version number of latest firmware | |
echo -n «Fetching version number of latest firmware… « | |
refresh_status_file @> /dev/null | |
# Parse status file | |
local fw_version=`cat $STATUS_FILE | jq -r .version 2> /dev/null` || fw_version=»» | |
local fw_url=`cat $STATUS_FILE | jq -r .url 2> /dev/null` || fw_version=»» | |
local fw_md5=`cat $STATUS_FILE | jq -r .md5 2> /dev/null` || fw_version=»» | |
local fw_state=`cat $STATUS_FILE | jq -r .state 2> /dev/null` || fw_version=»» | |
if [ -z «$fw_version» ] || [ «$fw_url» = «$DEFAULT_URL» ]; then | |
echo «failed» | |
exit 42 | |
else | |
echo «ok» | |
echo » > version : $fw_version» | |
echo » > url : $fw_url» | |
echo » > md5 : $fw_md5″ | |
echo » > state : $fw_state» | |
echo | |
fi | |
if [ «$fw_state» == «can-upgrade» ]; then | |
echo «New firmware $fw_version is available» | |
echo | |
sudo /usr/bin/ubnt-upgrade —upgrade-force-prompt «$fw_url» | |
elif [ «$fw_state» == «up-to-date» ]; then | |
echo «Current firmware is already up-to-date (!!!)» | |
echo | |
sudo /usr/bin/ubnt-upgrade —upgrade-force-prompt «$fw_url» | |
elif [ «$fw_state» == «reboot-needed» ]; then | |
echo «Reboot is needed before upgrading to version $fw_version» | |
else | |
echo «Upgrade is already in progress» | |
fi | |
} | |
#——————————————————————————- | |
if [ «$ACTION» == «refresh» ]; then | |
refresh_status_file | |
elif [ «$ACTION» == «status» ]; then | |
read_status_file | |
elif [ «$ACTION» == «upgrade» ]; then | |
upgrade_firmware | |
fi |
view rawCVE-2021-22909-snippet-2.bash hosted with ❤ by GitHub
The function proceeds to parse and compare the results from the server with the current firmware version. If an update is available, the script will invoke
The Bug — ZDI-21-601
The issue lies in the way the
get_tar_by_url () | |
{ | |
mkdir $TMP_DIR | |
if [ «$NOPROMPT» -eq 0 ]; then | |
echo «Trying to get upgrade file from $TAR» | |
fi | |
if [ -n «$USERNAME» ]; then | |
auth=»-u $USERNAME:$PASSWORD» | |
else | |
auth=»» | |
fi | |
filename=»${TMP_DIR}/${TAR##*/}» | |
if [ «$NOPROMPT» -eq 0 ]; then | |
curl -k $auth -f -L -o $filename $TAR # <—— | |
else | |
curl -k $auth -f -s -L -o $filename $TAR # <—— | |
fi | |
if [ $? -ne 0 ]; then | |
echo «Unable to get upgrade file from $TAR» | |
rm -f $filename | |
rm -f $DOWNLOADING | |
exit 1 | |
fi | |
if [ ! -e $filename ]; then | |
echo «Download of $TAR failed» | |
rm -f $DOWNLOADING | |
exit 1 | |
fi | |
if [ «$NOPROMPT» -eq 0 ]; then | |
echo «Download succeeded» | |
fi | |
TAR=$filename | |
} |
view rawCVE-2021-22909-snippet-3.bash hosted with ❤ by GitHub
Since
To exploit this vulnerability, the attacker can modify an existing EdgeRouter firmware image and fix up the checksum contained in the file. In the submitted proof-of-concept, the researcher modified the
Conclusion
If an attacker inserts themselves as MiTM, they can then impersonate the `fw-download.ubnt.com` domain controlled by Ubiquiti. However, to successfully serve up malicious firmware from this domain, the attackers would normally need to obtain a valid certificate with private key for the domain. To proceed, the attackers would probably need to hack into Ubiquiti or convince a trusted certificate authority (CA) to issue the attackers a certificate for the Ubiquiti domain, which is no insignificant feat. However, due to this bug, there is no need to obtain the certificate.
The heart of the problem is the lack of authentication on the firmware binary. The function of a secure communications channel is to provide confidentiality, integrity, and authentication. In TLS, encryption provides confidentiality, a message digest (or AEAD in the case of TLS 1.3) provides integrity, and certificate verification provides authentication. Without the verification of certificates, clients are foregoing authentication in the communications channel. In this scenario, it is possible for the client to be speaking to a malicious actor “securely”, as it were.
It should also be noted how checksums are not replacements for cryptographic signatures. Checksums can help to detect random errors in transmission but do not provide hard proof of data authenticity.
One final consideration is the possibility that a vendor’s website could become compromised. In that case, the firmware along with its associated hash could both be replaced with malicious versions. This situation can be mitigated only by applying a cryptographic signature to the firmware file itself. Perhaps Ubiquity will make the switch to signing their firmware binaries cryptographically, which would improve the overall security of its customers.
Ubiquity addressed this bug in their v2.0.9-hotfix.1 security update by removing the
This was the first submission to the program from awxylitol, and we hope to see more research from them in the future. Until then, you can find me on Twitter @TrendyTofu, and follow the team for the latest in exploit techniques and security patches.
Footnote
Cautious users of the EdgeRouter seeking advice on how to upgrade the device properly should avoid the use of automatic upgrade feature for this update. They may want to download the firmware file manually from a browser and verify the hashes of the firmware before performing a manual upgrade by uploading the firmware file to the device through the web interface.