CVE-2023-35963,CVE-2023-35960,CVE-2023-35964,CVE-2023-35959,CVE-2023-35961,CVE-2023-35962
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.
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
GTKWave - https://gtkwave.sourceforge.net
7.8 - CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-78 - Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)
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.
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.
vcd_main
legacy decompressionWhen 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.
vcd_recorder_main
decompressionWhen 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.
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.
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.
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.
Fixed in version 3.3.118, available from https://sourceforge.net/projects/gtkwave/files/gtkwave-3.3.118/
2023-07-10 - Initial Vendor Contact
2023-07-18 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.