CVE Wednesday - CVE-2022-40021

Introduction

Welcome to CVE Wednesday! Today’s CVE is going to be related to CVE-2022-40021. I say related to because I wasn’t able to find the firmware for the Amino A140, but poking around the QVidium Technologies website, I did find some old, unsupported firmware that also had several command injection vulnerabilities in it.

We’re really going to focus on command injection vulnerabilities in this write up.

From NVD:

QVidium Technologies Amino A140 (prior to firmware version 1.0.0-283) was discovered to contain a command injection vulnerability.

NVD lists this advisory link as the primary source for the CVE: https://www.securifera.com/advisories/CVE-2022-40021/. This advisory doesn’t give us a lot to go off of, but it does provide a link to the QVidium website: https://www.qvidium.com/. Looking around the Qvidium support section, I found https://www.qvidium.com/qvavc/, which appears to be an appliance firmware. Now, according to this changelog, the last time this firmware was updated was in October 2013 (Version 128). Indeed, it appears as thought the QVAVC was End of Life’d (EOL’d) almost a decade ago. Seeing as though the firmware is so old, the product is EOL’d, and I couldn’t even find pre-owned units for purchase on eBay, I’m going to list out some of the “probable” security vulnerabilities I found whilst reviewing the firmware. Like I said, I don’t have a physical unit to test against, but there are some patterns in the firmware code that lend themselves to different types of security vulnerabilities.

tl;dr: I’m full disclosing these issues.

Firmware Files

The firmware file for the webserver can be downloaded here and the kernel here.

$ file *
qvavc-128.pkg:                           POSIX tar archive (GNU)
qvavc-kernel-Jul-16-2010.pkg:            POSIX tar archive (GNU)

So we can untar the two *.pkg files and see what is inside.

User Accounts and Credentials

One of the first things I looked as was the user accounts built into the firmware image. The /etc/passwd file appears thusly:

root:$1$pkDzQNpi$HSq.3PhdjrVUav4yFlwYL0:0:0:root:/root:/bin/sh
www:x:80:80:root:/www:/bin/sh
nobody:x:99:99:Nobody:/:/sbin/nologin
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
admin:$1$7znC90go$SXYATSi4Np.IegTM1yoQ90:100:100:admin:/home/admin:/bin/sh

I ran these through hashcat with rockyou.txt as a very basic first pass towards cracking the passwords, and I got lucky:

$1$7znC90go$SXYATSi4Np.IegTM1yoQ90:adm1n
$1$pkDzQNpi$HSq.3PhdjrVUav4yFlwYL0:root

Hash cat Hash mode for these hashes are -m 500:

500 | md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5) | Operating System 

I then took a brief look at some of the compiled binaries on the firmware file system. I ran file against them to try to determine the CPU Architecture of the host:

ELF 32-bit LSB relocatable, ARC Cores Tangent-A5, version 1 (SYSV), not stripped

So we are looking at an ARC-based system.

Web Server

The web server built into the firmware image runs off of cgi. Before we begin digging into the scripts for vulnerabilities, there are two binaries in /flash/www/cgi-bin that we need to be aware of: cgiparse

The pattern for executing cgiparse looks like this:

eval ./cgiparse "$QUERY_STRING"

Effectively what this pattern does is take query string input passed through the $QUERY_STRING environment variable set by the CGI web process and turn each query string key-value pair into a environment variable with a value. So for example:

/url-path?foo=bar&coolguy=sagi would be transformed into:

foo=bar
coolguy=sagi
echo $foo
echo $coolguy

cgiparse

The cgiparse binary has multiple functions called off of main, but the ones that are most important is the function that parses and outputs characters:

// Memory Address: 0x00010478
_parse_query_string(int param_1, int param_2)
{
  size_t sVar1;
  undefined4 uStack_418;
  undefined2 uStack_40c;
  undefined auStack_40a [1022];
  int iStack_c;
  
  uStack_40c = 0x20;
  memset(auStack_40a,0,0x3fe);
  strcpy((char *)&uStack_40c,*(char **)(param_2 + 4));
  if (param_1 < 2) {
    printf("%s",&DAT_ram_000107e4);
    uStack_418 = 1;
  }
  else {
    iStack_c = 0;
    while (sVar1 = strlen((char *)&uStack_40c), iStack_c < (int)sVar1) {
        // Check if the current character is a +
        // and if it is output a space
      if (auStack_40a[iStack_c + -2] == '+') {
        putchar(0x20);
      }
      // check if the current character is a &
      // A & in HTTP specified a the beginning of a
      // new query string / form data parameter.
      // If it is an ampersand, output a double-quote 
      // to begin a string
      else if (auStack_40a[iStack_c + -2] == '&') {
        puts("\"");
      }
      // check if we have the query string key value
      // delimiter and if so, output the = followed by a "
      else if (auStack_40a[iStack_c + -2] == '=') {
        printf("%s",,"=\"");
      }
      else if ((auStack_40a[iStack_c + -2] == '\n') || (0x1f < (byte)auStack_40a[iStack_c + -2])) {
        // Handle URL encoded signifier and hex value
        if (auStack_40a[iStack_c + -2] == '%') {
          _validate_and_escape(&uStack_40c,&iStack_c);
        }
        else {
          putchar((uint)(byte)auStack_40a[iStack_c + -2]);
        }
      }
      else {
        putchar(0x20);
      }
      iStack_c = iStack_c + 1;
    }
    puts("\"");
    uStack_418 = 0;
  }
  return uStack_418;
}

_validate_and_escape looks like this:

// Memory Address: 0x00010658
_validate_and_escape(int param_1, int *param_2)
{
  int iVar1;
  int local_28;
  char local_1c;
  byte _current_character;
  
  iVar1 = *param_2 + 1;
  *param_2 = *param_2 + 2;
  if (*(byte *)(iVar1 + param_1) < 0x3a) {
    local_28 = 0;
  }
  else {
    local_28 = 7;
  }
  if (*(byte *)(param_1 + 1 + iVar1) < 0x3a) {
    local_1c = '\0';
  }
  else {
    local_1c = '\a';
  }
  _current_character = ((char)(((*(byte *)(iVar1 + param_1) - 0x30) - local_28 & 0xf) << 4) + (*(char *)(param_1 + 1 + iVar1) - local_1c)) - 0x30;
  // check if current character is " or `
  // and if it is, output a \ before it
  // remember friends, ` can be used to 
  // execute sub shells and " can be used to 
  // escape bash sequences!
  if ((_current_character == 0x22) || (_current_character == 0x60)) {
    putchar(0x5c);
  }
  // check if the current character is in 
  //the readable ASCII range and if it is not, 
  // output a space
  else if ((_current_character < 0x20) || (0x7e < _current_character)) {
    _current_character = 0x20;
  }
  putchar((uint)_current_character);
  return 0;
}

When executed, this will take a value like sudi=sagi&tah=kagi and then cgiparse would output something like:

sudi=sagi
tah=kagi

which would then be eval and turned into environment variables!

Parallel Test

Here is a small equivalent bash script that demonstrates what happens when the value $(echo lolololololol) is used as an HTTP query string parameter value.

#!/bin/bash
QUERY_STRING='foo=bar \
bat=$(echo lolololololol)'
eval `echo "$QUERY_STRING"`

When run:

test.sh: line 4:  bat=lolololololol: command not found

This means EVERYWHERE cgiparse is used with $QUERY_STRING is vulnerable to command-injection.

Here is a list of files that use this pattern:

$ grep -r "cgiparse" .
./change_password.cgi:eval `./cgiparse "$QUERY_STRING"`
./decoder-setup.cgi:    eval `./cgiparse "$QUERY_STRING"`
./encoder-setup.cgi:    eval `./cgiparse "$QUERY_STRING"`
./encoder_bitrate.cgi:    eval `./cgiparse "$QUERY_STRING"`
./encoder_bitrate2.cgi:    eval `./cgiparse "$QUERY_STRING"`
./encoder_set_dest.cgi:    eval `./cgiparse "$QUERY_STRING"`
./encoder_volume.cgi:    eval `./cgiparse "$QUERY_STRING"`
./logging_conf.cgi:    eval `./cgiparse "$QUERY_STRING"`
./mcast_conf.cgi:    eval `./cgiparse "$QUERY_STRING"`
./mgmt_conf.cgi:    eval `./cgiparse "$QUERY_STRING"`
./mgmt_snmp_trap.cgi:  eval `./cgiparse "$QUERY_STRING"`
./network-setup.cgi:  eval `./cgiparse "$QUERY_STRING"`
./network_ipaddrs.cgi:  eval `./cgiparse "$QUERY_STRING"`
./network_ping.cgi:    eval `./cgiparse "$QUERY_STRING"`
./network_routes.cgi:  eval `./cgiparse "$QUERY_STRING"`
./network_traceroute.cgi:    eval `./cgiparse "$QUERY_STRING"`
./ping_conf.cgi:    eval `./cgiparse "$QUERY_STRING"`
./syslog_conf.cgi:    eval `./cgiparse "$QUERY_STRING"`
./system_hostname.cgi:  eval `./cgiparse "$QUERY_STRING"`
./system_name.cgi:  eval `./cgiparse "$QUERY_STRING"`
./system_time.cgi:    eval `./cgiparse "$QUERY_STRING"`
./transcode-setup.cgi:    eval `./cgiparse "$QUERY_STRING"`

This is 21 different command injections!

Home