Huawei NetEngine AR617VW Authenticated Root RaCE
TL;DR
The journey of finding two authenticated vulnerabilities in Huawei NetEngine AR617VW and chaining them to achieve RCE as root.
- CVE-2022-48615: Improper Access Control in OPS Script Assistant.
- CVE-2022-48616: Command Injection in OPS Script Assistant File Name.
Introduction
Huawei uses a general network operating system called Versatile Routing Platform (VRP) for their enterprise-grade routers. More about VRP can be read here.
https://forum.huawei.com/enterprise/en/VRP-Basics/thread/667280632188911616-667213852955258880
However, during the inspection of the firmware using binwalk
a Linux kernel is found along with two squashfs filesystems.
|
|
After extracting the filesystem and skimming through it, a huge binary (~180MB) named vrp
is found in a directory named /mpu/
inside the second squashfs. This binary acts as a guest OS on top of the Linux host as it was understood from analysis.
The router has many physical ports available but the main focus in this blogpost are:
- Console: A serial ethernet connection showing boot log and prompts for authentication for CLI access.
- Mgmt/GE0: The ethernet port on which the management portal is enabled by default.
This serial interface operates over a baudrate of 9600 and here’s the boot log of the router:
|
|
And being connected to the Mgmt allows us to access the management portal:
By default only the web management portal is enabled on the Mgmt port. For the sake of analysis, telnet and ftp were also enabled from the web portal.
Analysis
An interesting functionality that instantly drove the focus was the Open Programmability System (OPS) which is a feature by Huawei to allow the extending of the routers functionalities by providing APIs.
Open Programmability System
The Open Programmability System (OPS) is an open platform that provides Application Programming Interfaces (APIs) to achieve device programmability, allowing third-party applications to run on the device.
How it works?
The below flowchart represents the full work process of the OPS.
https://support.huawei.com/enterprise/en/doc/EDOC1100034067/e44d6749/understanding-ops
This flowchart can be translated to the following commands:
|
|
Digging Deeper
Scripts are not being executed on their own instead they are being imported as modules by /mnt/squash/local/pyhome/lib/frame.py
as shown in the vrp
binary’s function OPS_ProcMng_ExecuteWithArg
The /mnt/squash/local/pyhome/lib/frame.py
does the following:
Parse the arguments provided.
1 2 3 4 5 6 7
try: arg_list, unknown_arg_list = getopt.getopt(sys.argv[1:], "", ["phase=", "script-dir=", "script-name=", "main-queue-id=", "slave-queue-id=", "old-dir=", "new-dir="]) except getopt.GetoptError as err: ... arg = farg.frame_args(arg_list) ...
Create an OPS object.
1 2 3 4
import ops ... o = ops.Ops(arg) ...
Import the module and check if it is valid.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
def IsValidModule(m): has_condition = False has_execute = False for ele in dir(m): if ele == 'ops_condition': has_condition = True if ele == 'ops_execute': has_execute = True if has_condition and has_execute: return True return False ... try: m = __import__(arg.module) except: ... if not IsValidModule(m): ...
Execute module function based on the phase.
1 2 3 4 5 6 7 8 9
try: if arg.phase == 'condition': dbg("will execute ops_condition()...") ret = m.ops_condition(o) else: dbg("will execute ops_execute()...") ret = m.ops_execute(o) except: ...
By referring to scripts provided in the firmware in /mnt/squash/etc/libscript/
it was noticed that to print values to the connected terminal the function o.vtyprint
is used with o
referring to the ops
object. Meanwhile the normal print
function has its output directed to the console port (STDIN
, STDOUT
and STDERR
are all redirected to the console port).
In the ops.py
module the vtyprint
function is implemented as:
|
|
With this implementation in mind it was possible to directly call _ops.vtyprint
with the script name and the message to be printed.
A simple script verifying this will look like this:
|
|
And proceeding to running the script using the steps mentioned above succeeds as shown.
It is worth noting that the script was executed on being imported. However the bounding to a script assistant failed as we do not follow the intended script structure (as it won’t be needed in our case). If we were to proceed with using the same script name test.py
we’ll need to uninstall it and install the modified version.
First Vulnerability (CVE-2022-48615)
A script run_test.py
was written to ease the process of running OPS script, it uploads the file through FTP and executes it over telnet using the previously mentioned commands.
After some trial and error the following were deducted:
- Not all base python modules were available.
system
,exec
,execv
, and other functions used to run OS commands inside theos
module were not available.open
can only access files inside/mnt/flash/
directory.os.open
was able to access files outside the/mnt/flash
directory.
Triaging
To verify that it is possible to read outside the jail directory /mnt/flash
using os.open
, a script that reads the content of /etc/passwd
would do the trick:
|
|
Running the script actually read the contents of /etc/passwd
Exploitation
Other than leveraging this access control vulnerability to read and/or write files, it can lead to RCE by utilizing both the read and write primitives. The main idea to achieve this is to manipulate the python process memory and giving ourselves access to the system
function.
This was achieved using the following approach:
- Read the contents of
/proc/self/maps
- Parse the base address for the python binary and libc.
- Calculate the offset of
mkdir@got
- Calculate the offset of
system
in libc. - Overwrite the value of
mkdir@got
withsystem
Performing this correctly results in converting the os.mkdir
function into os.system
. The script to achieve this is as follows:
|
|
Running the script, it is noticed that commands are being executed as the user python
Further Analysis
Achieving RCE is fun but not as python
user so let’s dig more into how the OPS handle python scripts. To do this we will have a look in the vrp
binary.
Circling back to the same 4 operations mentioned for the OPS, the following functions take the spotlight:
|
|
But our focus will only be directed to OPS_Cfg_ScriptAssistInstall
as this is the function responsible for running our script.
It performs a couple of checks on the script’s filename, the checks are as follows:
OPS_Cfg_IsPythonFile
: Checks if the filename has only one dot appended by the python extension (.py
)OPS_Cfg_IsPythonInternalFile
: Checks if the filename matches any of the python internal modules such asos.py
OPS_Cli_IsExecuteCliByFileName
: Checks if the script is already running.
Then comes the interesting part:
- First the file path is being built by concatenating the
defaultResourcePath
which is/mnt/flash
with the string“$_user”
and the filename. So the final string will look like this"/mnt/flash/$_user/$FILENAME"
- It then builds a
chmod
command with the resultingfilePath
- A check is preformed to make sure that the script being executed is not in the
pys
directory. - If the conditions are met,
PAT_SystemCall
is called with thechmod
command.
Second Vulnerability (CVE-2022-48616)
As mentioned the command
is being prepared using the sprintf_s
function without any prior validation on the fileName
which presents a command injection vulnerability but with some constraints:
- Filename length must be less than 65 bytes.
- Filename must only contain one dot.
- Filename must end with
.py
- Filename cannot contain any of these characters
["<", ">", ":", "/", "\\", "|", "\"", "'", "*", "~", " "]
Triaging
The simplest way to verify the vulnerability is to upload a file with the name ;reboot;.py
The telnet session is closed and the router is rebooting which clearly indicates that the command injection is working.
How about trying to get an interactive shell? Would that work? Let’s give it a shot by uploading a file with the name ;sh;.py
The telnet session just hangs after running the script assistant command. However, on the console port a root shell appeared.
So, now we can run commands as root
but STDIN
apparently is overlapping between the login and our shell. Anyway, we still need to get an RCE not just execute commands over the console port.
Exploitation
After a few trial and error, the following was concluded:
- The current directory is not writable.
- Directory traversal is not allowed because only one dot is allowed in the filename.
- Absolute paths were not usable as the forward slash is blocked.
- I/O redirection is not possible.
- The command size limit is
64 - 3
At the time of writing, I am not aware of any ways that can allow bypassing these restrictions without leveraging other vulnerabilities.
It seemed like a dead end… Well, not yet!
Chain to RaCE
A little recap over steps that will help achieve the objective:
- Create a file.
- Make it executable.
- Write payload to the file.
- Execute the file.
After some refreshing research, it was possible to create a file and reference its name.
The payload looks like this ;t=`mktemp`;cat${IFS}$t;.py
t=`mktemp`
: Will create a temp file and store its name in$t
${IFS}
: Used instead of spaces.- The
cat
command here is just a placeholder to imply that we can access the file in$t
This allowed the bypass of a couple of the restrictions mentioned earlier. Also, the payload can be simply modified into ;t=`mktemp`;chmod${IFS}777${IFS}$t;sh${IFS}$t;.py
and now we checked 3 out of 4 steps.
Plan
Recall the first vulnerability, it was possible to read and write to files anywhere on the system. Let’s leverage that and chain it with the second vulnerability. The idea is to create a file and modify its permissions using the command injection then use the first vulnerability to write out payload into the file and finally execute the file using the second vulnerability. But this has to be done in parallel in order not to lose the reference to the file.
This is a simple illustration for the planned race:
Achieving RCE as root
As we kind of have our command injection payload, lets start with the python script that will write to our temp file. A simple script (race.py
) that will loop over all temp files and write a bash script into them.
|
|
To start writing the exploit, some helpers were written to ease the process:
|
|
The main function of the exploit will look like this:
|
|
The main
function of the exploit performs the following:
- Create a python file with the command injection payload.
- Upload both the
race.py
and command injection payload file using FTP. - Open two telnet sessions.
- Start a child process.
- Child process runs the command injection payload.
- Parent process sleeps for 2 seconds to make sure the temp file is created.
- Parent process runs the
race.py
script. - Finally, the parent process sleeps for 3 seconds to wait for the command to be executed and then read the
RCE
file.
By running the script we are easily running commands as root
and getting their results. However we are a little bit greedy and want to get an interactive reverse shell.
Getting a Reverse Shell
After several trials to get outbound connection, it was not possible so let’s try to understand by modifying the bash script we are executing and get the output of ifconfig
Apparently there are no interfaces on the Linux host that allows routing to the internal network. After some research, the understanding that was reached (which might be wrong) is that that Huawei’s VRP act as a guest OS which runs on top of the Host OS (Linux) and utilizes some of its functionalities. So the routing was handled by the vrp
binary. And given this public resource from Huawei, it was understood that to be able to get the reverse shell, the routing must be configured from the VRP CLI to enable a virtual interface on the Host OS. This is the sequence of commands to achieve this:
|
|
After running these commands and inspecting the result of ifconfig
, a new interface (veth1
) is shown.
Applying some minor adjustments to the exploit to handle the receiving of a reverse shell and VOILA!
For the sake of completion, additional code was added to the exploit to authenticate with the web interface and make sure that the FTP and Telnet services are enabled and the user used have access to these services.
Affected Systems and Mitigations
These vulnerabilities were discovered on AR610VW router running AR600-V300R21C00SPC200. However, they may affect other routers and versions. Please refer to Huawei for clarification on vulnerable routers and/or versions and their respective patched version.
Disclosure Timeline
- Discovery: April 10, 2022
- Reported to Vendor: June 02, 2022
- Patching: September 07, 2022 - September 20, 2022
- Disclosure: October 31, 2023
Acknowledgement
Saif Aziz (@wr3nchsr) of CyShield
Disclaimer
CYSHIELD FOR TECHNOLOGY S.A.E (CYSHIELD) are keen to share our expertise widely and to enrich public knowledge, through disseminating cyber security culture awareness. CYSHEILD relies on information provided by the vendor / product manufacturer when listing fixed versions, products or releases. CYSHIELD does not verify this contained information, except otherwise when specifically stipulated in the advisory text and contractually required or explicitly agreed in a written form by the vendor / product manufacturer to undertake as such. Unconfirmed vendor / product manufacturer fixes might be ineffective, incomplete, inaccurate or easy to bypass and it is the vendor’s / product manufacturer’s liability to ensure all the discovered vulnerabilities found by CYSHIELD are resolved properly. CYSHIELD accepts zero liability, financial or otherwise, from any material and consequential losses, loss of life or reputational loss arising from or as a result of misuse of the information or code contained or mentioned in its advisories. It is the vendor’s / product manufacturer’s liability not CYSHIELD to ensure their products’ security before, during and after release to market.