Analyzing CVE-2022-4883 (PATH Hijacking in libxpm)

About The Project

CVE-2022-4883 outlines a Linux PATH hijacking vulnerability in the libxpm package. Libxpm is used in a variety of projects to parse “X Pixmap” images. The National Vulnerability Database rates this vulnerability at a CVSS score of 8.8 and Red Hat has given it a CVSS score of 8.1. Per, the Arch Linux package page, 39 packages currently list libxpm as a dependency. This blog post will walk through the vulnerability and exploitation of said vulnerability.

Analyzing the Security Advisory & Patch

The CVE advisory states:

A flaw was found in libXpm. When processing files with .Z or .gz extensions, the library calls external programs to compress and uncompress files, relying on the PATH environment variable to find these programs, which could allow a malicious user to execute other programs by manipulating the PATH environment variable.

Following, the official Xorg mailing list, the patch below shows the second argument of the xpmPipeThrough function being replaces with “uncompress” and “gunzip” with a const value of XPM_PATH_UNCOMPRESS and XPM_PATH_GUNZIP.

	-   mdata->stream.file = xpmPipeThrough(fd, "uncompress", "-c", "r");
	+   mdata->stream.file = xpmPipeThrough(fd, XPM_PATH_UNCOMPRESS, "-c", "r");
	}
	else if ( ext && !strcmp(ext, ".gz") )
	{
	    mdata->type = XPMPIPE;
	-   mdata->stream.file = xpmPipeThrough(fd, "gunzip", "-qc", "r");
	+   mdata->stream.file = xpmPipeThrough(fd, XPM_PATH_GUNZIP, "-qc", "r");
	}

xpmPipeThrough is effectively a wrapper for forking and execing the arguments seen above. An abbreviated snippet below highlights said dangerous snippet. Since the PATH variable of the user executing the code is searched, its possible to execute arbitrary code via changing the PATH variable. Depending on the context of how this code is run, there may be potential for privilege escalation as well. The patch addresses this issue by resolving to the absolute path of the gzip binary.

	close(fds[in]);
	if ( dup2(fds[out], out) < 0 )
	    goto err;
	close(fds[out]);
	if ( dup2(fd, in) < 0 )
	    goto err;
	close(fd);
	pid = fork();
	if ( pid < 0 )
	    goto err;
	if ( 0 == pid )
	{
#ifdef HAVE_CLOSEFROM
	    closefrom(3);
#elif defined(HAVE_CLOSE_RANGE)
# ifdef CLOSE_RANGE_UNSHARE
#  define close_range_flags CLOSE_RANGE_UNSHARE
# else
#  define close_range_flags 0
#endif
	    close_range(3, ~0U, close_range_flags);
#endif
	    execl(cmd, cmd, arg1, (char *)NULL);            <--- here be dragons!
	    perror(cmd);
	    goto err;
	}
	_exit(0);

Recreating The Issue

The libxpm repo contains a small example program called cxpm that calls the vulnerable OpenFileRead function. Simply cloning a vulnerable release of the code (Ex: 3.5.12), and building the project gives an adequate environment to exploit the vulnerability. Gimp, the Open Source image editing tool allows the end user to save a file as a “XPM” extension. Next, the end user can create a tar.gz file with a XPM image which will then trigger the code path to the vulnerable fork-exec code path. Then, by modifying the environment’s PATH variable (export PATH=/tmp/tmppath:$PATH) an example binary with the name of “gzip” can be placed in the new directory to be triggered during the analysis.

The exploitation of this bug can be shown visually through a gdb session where the variable set follow-fork-mode is set to child. This enables the end user to follow the exec and see the new highjacked gzip spawn.

  1. The breakpoint is hit where the file extension of our image is being examined to see if ends with .gz.
  2. We see that the path has led us to fork giving us a new PID of 11528.
  3. Bash has been spawned
  4. Our gzip is being executed from /tmp/tmppath.

./cve_2022_4883.png

The newly created gzip is a simple C program that simply writes “pwned!” to tmp/lol.txt. A more elaborate PoC is left as an exercise to the reader.

Beyond The Blog

LibXpm versions 3.5.15 and beyond are patched against this bug. However, something I don’t understand is that Red Hat’s security advisory lists the attack vector as network based. I assume this is an error, or there’s something else here I didn’t capture. Thank you for reading!