Talos Vulnerability Report

TALOS-2023-1786

GTKWave decompression OS command injection vulnerabilities

January 8, 2024
CVE Number

CVE-2023-35963,CVE-2023-35960,CVE-2023-35964,CVE-2023-35959,CVE-2023-35961,CVE-2023-35962

SUMMARY

Multiple OS command injection vulnerabilities exist in the decompression functionality of GTKWave 3.3.115. A specially crafted wave file can lead to arbitrary command execution. A victim would need to open a malicious file to trigger these vulnerabilities.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

GTKWave 3.3.115

PRODUCT URLS

GTKWave - https://gtkwave.sourceforge.net

CVSSv3 SCORE

7.8 - CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-78 - Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)

DETAILS

GTKWave is a wave viewer, often used to analyze FPGA simulations and logic analyzer captures. It uses a graphical user interface to convert the traces across several file formats (.lxt, .lxt2, .vzt, .fst, .ghw, .vcd, .evcd) either by using the UI or its command line tools. GTKWave is available for Linux, Windows and MacOS. Trace files can be shared within teams or organizations, for example to compare results of simulation runs across different design implementations, to analyze protocols captured with logic analyzers or just as a reference when porting design implementations.

GTKWave sets up mime types for its supported extensions, so for example it’s enough for a victim to double-click on a wave file received by e-mail to trigger the vulnerabilities described in this advisory.

GTKWave supports opening wave files compressed with gzip, bzip2, or zip. For decompression, external programs are used and executed against input files by using popen. This leads to trivial command injections on the input file names. The different vulnerable flows are described separately.

CVE-2023-35959 - GHW decompression

When gtkwave opens a file, the function main_2 is called. This is a pretty long function, so we’re going to highlight only the lines needed to trace the issue.

When a .ghw, .ghw.gz or .ghw.bz2 file is opened, we enter the condition at main.c:1771, and ghw_main() is called:

else if (suffix_check(GLOBALS->loaded_file_name, ".ghw") || suffix_check(GLOBALS->loaded_file_name, ".ghw.gz") ||
        suffix_check(GLOBALS->loaded_file_name, ".ghw.bz2"))
    {
          GLOBALS->loaded_file_type = GHW_FILE;
[1]   if(!ghw_main(GLOBALS->loaded_file_name))
        {
        /* error message printed in ghw_main() */
        vcd_exit(255);
        }
    }

GLOBALS->loaded_file_name contains the name of the input file.

ghw_main() in turn calls ghw_open(), where filename is the name of the input file:

    ghw_open (struct ghw_handler *h, const char *filename)
    {
      char hdr[16];

[2]   h->stream = fopen (filename, "rb");
      if (h->stream == NULL)
        return -1;

[3]   if (fread (hdr, sizeof (hdr), 1, h->stream) != 1)
        return -1;

      /* Check compression layer.  */
[4]   if (!memcmp (hdr, "\x1f\x8b", 2))
        {
[5]       if (ghw_openz (h, "gzip -cd", filename) < 0)
        return -1;
          if (fread (hdr, sizeof (hdr), 1, h->stream) != 1)
        return -1;
        }
[4]   else if (!memcmp (hdr, "BZ", 2))
        {
[5]        if (ghw_openz (h, "bzip2 -cd", filename) < 0)
        return -1;
          if (fread (hdr, sizeof (hdr), 1, h->stream) != 1)
        return -1;
        }
      else
        {
          h->stream_ispipe = 0;
        }

At [2] the input file is opened and the first 16 bytes are read (sizeof(hdr)). If the file starts with a gzip or bzip2 magic [4] (\x1f\x8b or BZ), ghw_openz() is called, passing the filename as second argument [6].

    static int
    ghw_openz (struct ghw_handler *h, const char *decomp, const char *filename)
    {
      int plen = strlen (decomp) + 1 + strlen (filename) + 1;
      char *p = malloc (plen);

[6]   snprintf (p, plen, "%s %s", decomp, filename);
      fclose (h->stream);
[7]   h->stream = popen (p, "r");
      free (p);

      if (h->stream == NULL)
        return -1;

      h->stream_ispipe = 1;

      return 0;
    }

This function concatenates the decompression command with the input file name [6] and passes it to popen, which means p is executed as a shell command. As filename has not been sanitized and popen is used, this leads to a command injection via the input file name.

CVE-2023-35960 - vcd_main legacy decompression

When gtkwave opens a file, the function main_2 is called. This is a pretty long function, so we’re going to highlight only the lines needed to trace the issue.

When the -L switch is given, gtkwave will try to open VCD files in legacy mode, setting the is_legacy variable to 1 inside the main_2 function. This will land us at main.c:1830:

    if(is_legacy)
        {
          GLOBALS->loaded_file_type = (strcmp(GLOBALS->loaded_file_name, "-vcd")) ? VCD_FILE : DUMPLESS_FILE;
[1]       vcd_main(GLOBALS->loaded_file_name);
        }

At [1] vcd_main() is called, passing the input file name as argument.

    #define WAVE_DECOMPRESSOR "gzip -cd "   /* zcat alone doesn't cut it for AIX */
    ...

    TimeType vcd_main(char *fname)
    {
    ...
[2] if(suffix_check(fname, ".gz") || suffix_check(fname, ".zip"))
        {
        char *str;
        int dlen;
        dlen=strlen(WAVE_DECOMPRESSOR);
        str=wave_alloca(strlen(fname)+dlen+1);
        strcpy(str,WAVE_DECOMPRESSOR);
[3]     strcpy(str+dlen,fname);
[4]     GLOBALS->vcd_handle_vcd_c_1=popen(str,"r");
        GLOBALS->vcd_is_compressed_vcd_c_1=~0;
        }

Inside vcd_main(), if the input file name ends with .gz or .zip [2], a command is built by concatenating the WAVE_DECOMPRESSOR string to the input file name [3] and passed to popen [4], which means str is executed as a shell command. As filename has not been sanitized and popen is used, this leads to a command injection via the input file name.

CVE-2023-35961 - vcd_recorder_main decompression

When gtkwave opens a file, the function main_2 is called. This is a pretty long function, so we’re going to highlight only the lines needed to trace the issue.

If the -L switch is not used we enter the block at [1].

    if(is_legacy)
        {
          GLOBALS->loaded_file_type = (strcmp(GLOBALS->loaded_file_name, "-vcd")) ? VCD_FILE : DUMPLESS_FILE;
          vcd_main(GLOBALS->loaded_file_name);
        }
        else
[1]     {
          if(strcmp(GLOBALS->loaded_file_name, "-vcd"))
            {
            GLOBALS->loaded_file_type = VCD_RECODER_FILE;
            GLOBALS->use_fastload = is_fastload;
            }
            else
            {
            GLOBALS->loaded_file_type = DUMPLESS_FILE;
            GLOBALS->use_fastload = VCD_FSL_NONE;
            }
[2]       vcd_recoder_main(GLOBALS->loaded_file_name);
        }

In this case, gtkwave will open VCD files using vcd_recoder_main() [2], passing the input file name as argument.

    #define WAVE_DECOMPRESSOR "gzip -cd "   /* zcat alone doesn't cut it for AIX */
    ...

    TimeType vcd_recoder_main(char *fname)
    {
    ...

[3]  if(suffix_check(fname, ".gz") || suffix_check(fname, ".zip"))
        {
        char *str;
        int dlen;
        dlen=strlen(WAVE_DECOMPRESSOR);
        str=wave_alloca(strlen(fname)+dlen+1);
        strcpy(str,WAVE_DECOMPRESSOR);
[4]     strcpy(str+dlen,fname);
[5]     GLOBALS->vcd_handle_vcd_recoder_c_2=popen(str,"r");
        GLOBALS->vcd_is_compressed_vcd_recoder_c_2=~0;
        }

Inside vcd_recoder_main(), if the input file name ends with .gz or .zip [3], a command is built by concatenating the WAVE_DECOMPRESSOR string to the input file name [4] and passed to popen [5], which means str is executed as a shell command. As filename has not been sanitized and popen is used, this leads to a command injection via the input file name.

CVE-2023-35962 - vcd2vzt vcd_main decompression

When vcd2vzt opens a file, it calls vcd_main() in vcd2vzt.c to parse the input file, passing the input file name as first argument.

    #define WAVE_DECOMPRESSOR "gzip -cd "   /* zcat alone doesn't cut it for AIX */
    ...

    TimeType vcd_main(char *fname, char *lxname)
    {
    ...
[1] if((strlen(fname)>2)&&(!strcmp(fname+strlen(fname)-3,".gz")))
        {
        char *str;
        int dlen;
        dlen=strlen(WAVE_DECOMPRESSOR);
        str=(char *)wave_alloca(strlen(fname)+dlen+1);
        strcpy(str,WAVE_DECOMPRESSOR);
[2]     strcpy(str+dlen,fname);
[3]     vcd_handle=popen(str,"r");
        vcd_is_compressed=~0;

Inside vcd_main(), if the input file name ends with .gz [1], a command is built by concatenating the WAVE_DECOMPRESSOR string to the input file name [2] and passed to popen [3], which means str is executed as a shell command. As filename has not been sanitized and popen is used, this leads to a command injection via the input file name.

CVE-2023-35963 - vcd2lxt2 vcd_main decompression

When vcd2lxt2 opens a file, it calls vcd_main() in vcd2lxt2.c to parse the input file, passing the input file name as first argument.

    #define WAVE_DECOMPRESSOR "gzip -cd "   /* zcat alone doesn't cut it for AIX */
    ...

    TimeType vcd_main(char *fname, char *lxname)
    {
    ...
[1] if((strlen(fname)>2)&&(!strcmp(fname+strlen(fname)-3,".gz")))
        {
        char *str;
        int dlen;
        dlen=strlen(WAVE_DECOMPRESSOR);
        str=(char *)wave_alloca(strlen(fname)+dlen+1);
        strcpy(str,WAVE_DECOMPRESSOR);
[2]     strcpy(str+dlen,fname);
[3]     vcd_handle=popen(str,"r");
        vcd_is_compressed=~0;

Inside vcd_main(), if the input file name ends with .gz [1], a command is built by concatenating the WAVE_DECOMPRESSOR string to the input file name [2] and passed to popen [3], which means str is executed as a shell command. As filename has not been sanitized and popen is used, this leads to a command injection via the input file name.

CVE-2023-35964 - vcd2lxt vcd_main decompression

When vcd2lxt opens a file, it calls vcd_main() in vcd2lxt.c to parse the input file, passing the input file name as first argument.

    #define WAVE_DECOMPRESSOR "gzip -cd "   /* zcat alone doesn't cut it for AIX */
    ...

    TimeType vcd_main(char *fname, char *lxname, int dostats, int doclock, int dochg, int dodict, int linear)
    {
    ...
[1] if((strlen(fname)>2)&&(!strcmp(fname+strlen(fname)-3,".gz")))
        {
        char *str;
        int dlen;
        dlen=strlen(WAVE_DECOMPRESSOR);
        str=(char *)wave_alloca(strlen(fname)+dlen+1);
        strcpy(str,WAVE_DECOMPRESSOR);
[2]     strcpy(str+dlen,fname);
[3]     vcd_handle=popen(str,"r");
        vcd_is_compressed=~0;

Inside vcd_main(), if the input file name ends with .gz [1], a command is built by concatenating the WAVE_DECOMPRESSOR string to the input file name [2] and passed to popen [3], which means str is executed as a shell command. As filename has not been sanitized and popen is used, this leads to a command injection via the input file name.

VENDOR RESPONSE

Fixed in version 3.3.118, available from https://sourceforge.net/projects/gtkwave/files/gtkwave-3.3.118/

TIMELINE

2023-07-10 - Initial Vendor Contact
2023-07-18 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.