CVE-2023–36664: Command injection with Ghostscript

vsociety
18 min readAug 18, 2023

--

Summary

A vulnerability denoted as CVE-2023–36664 emerged in Ghostscript versions prior to 10.01.2. This vulnerability has been attributed a sky-high CVSS score of 9.8, signifying its potential to facilitate code execution.

Description

Introduction

Summary of the blog

To delve into a detailed CVE analysis, let’s begin by examining the recent revelation concerning Ghostscript, an open-source interpreter utilized for PostScript language and PDF files.

A vulnerability denoted as CVE-2023–36664 emerged in versions prior to 10.01.2. This vulnerability has been attributed a sky-high CVSS score of 9.8, signifying its potential to facilitate code execution.

The root cause lies in Ghostscript’s mishandling of permission validation for pipe devices, where the misuse of %pipe% or the | pipe character prefix creates an exploitable avenue.

Here is the official description:

Artifex Ghostscript through 10.01.2 mishandles permission validation for pipe devices (with the %pipe% prefix or the | pipe character prefix).

Debian Security Advisory also writes about a possible execution of arbitrary commands here:

“It was discovered that Ghostscript, the GPL PostScript/PDF interpreter, does not properly handle permission validation for pipe devices, which could result in the execution of arbitrary commands if malformed document files are processed.”

What is Ghostscript?

Ghostscript is an open-source software suite and interpreter that is primarily used for rendering and interpreting files written in the PostScript (PS) and PDF languages. Developed originally by L. Peter Deutsch in 1988, Ghostscript has since evolved into a versatile tool widely utilized for tasks such as displaying and printing PostScript and PDF documents, converting these formats into various image formats, and even serving as a basis for some printer drivers.

Ghostscript is particularly valuable in scenarios where you need to manipulate or convert documents between different formats. It’s often used in the printing industry, as well as in software applications that deal with document rendering, conversion, and manipulation.

The most important things to know about it:

  • The software is available under the GNU General Public License (GPL) and the Affero General Public License (AGPL), which means it can be freely used, modified, and distributed by the community.
  • Its flexibility with a wide range of capabilities.
  • It’s open-source and frequently used by other open-source software packages.
  • One of the paramount utilities that rely on Ghostscript is “cups-filters,” an indispensable component woven into the fabric of the Common Unix Printing System (CUPS). On a Debian 12 system, 131 packages depend on Ghostscript.
  • Also used by other apps like LibreOffice, Inkscape, and Scribus, along with other tools such as ImageMagick.
  • Also ported to Windows so the vulnerability also affects Windows.

Lab Setup

Get Ghostscript

The official git repos of the vendor are here and here. You can find Ghostscript releases here.

I’ve cloned a vulnerable version of Ghostscript to build locally from the GitHub repo. The last version before the patch is 10.01.1 so I started with that:

git clone -b gs10.01.1 https://github.com/ArtifexSoftware/ghostpdl.git

Sidenote: since version 9.50 Ghostscript introduced the -dNOSAFER argument that is needed to exploit the vulnerability because that is related to file write. From that version safe mode (-dSAFER) is a default. See the details in the official docs but in short, this flag disables SAFER mode until the .setsafe procedure is run. This is intended for clients or scripts that cannot operate in SAFER mode. If Ghostscript is started with -dNOSAFER or -dDELAYSAFER, PostScript programs are allowed to read, write, rename, or delete any files in the system that are not protected by operating system permissions.

Get VS2022

To debug Ghostscript we download Visual Studio 2022 Community IDE installer from the official website.

When running, select “Desktop Development with C++” and “Visual Studio extension development” before installing.

Also, don’t forget to select Windows 10 SDK (or Windows 11 if using that):

Then, you can finally install the packages.

After installation make sure that nmake is on the PATH. Start a new command line with WIN + R and write "cmd" into that. Then, just run "nmake" and see the results. You should see this:

Otherwise set the location of nmake in the PATH variable. If you are not familiar with this topic, see this article.

See how to build Ghostscript from source code on the official site here.

Setting up the debugging environment

Load the ghostpdl-10.01.1\windows\GhostPDL.sln file into VS and select Build/Build Ghostscript.

This will take a while but at the end of the process some executables into the ghostpdl-10.01.1\profbin directory. We will use gswin64c.exe.

Static analysis

Let’s see the background and the code itself before moving forward and reproducing the vulnerability.

Pipes

The heart of the vulnerability lies in Unix-like operating system pipelines, which serve as a conduit for distinct software components to communicate by channeling the output of one application as the input for another. Represented by the iconic vertical bar symbol “|" these pipelines are a cornerstone of command-line interaction. An illustrative instance would be:

cat file.txt | wc -l

This command counts the number of lines in file.txt by piping the content to the wc (word count) command with the -l flag.

cat text.txt | sed 's/old/new/g'

This uses the sed command to replace occurrences of "old" with "new" in the content of text.txt.

Ghostscript also supports pipes within a command for both input and output operations. Here are the official docs.

What is important to us now is the %pipe% syntax that is used when someone wants to generate a file and directly pipe it to another app. Take this command:

gswin64c.exe -sOutputFile=%pipe%notepad [...]

This command equals to this (both pipes the output to notepad):

gswin64c.exe -sOutputFile==%stdout -q [...] | notepad

Permission validation

Forgive me but I would like to share the journey of exploring the exploitation path too, not just the final solution.

The vulnerability description also says that the issue relates to permissions validation. A small reminder:

Artifex Ghostscript through 10.01.2 mishandles permission validation for pipe devices (with the %pipe% prefix or the | pipe character prefix).

Based on this it was not obvious to find out where the vulnerability actually lies. In the only-one PoC and analysis I could find the injected code runs when opening an EPS file and based on that article it is also suggested that the issue is with the write permission but I left this question open.

As a next step, I checked the commit of the fix here and saw that basically two files are affected:

  • base/gpmisc.c
  • base/gslibctx.c

The fixes are quite similar.

Look at this note in the fixed code:

"%pipe%" do not follow the normal rules for path definitions, so we don't "reduce" them to avoid unexpected results

Could it be that utilizing a string that starts with “%pipe%" might serve as the path? And could there be an issue with how this path is being improperly "reduced"? We will see.

Open base/gpmisc.c that is affected in the patch in VS 2022. I searched for something related to permission validation and this took me to a method called "gp_validate_path_len". Here there is a switch for validating permission for a given file:

The validate method — surprise, surprise — validates a file path for a given permission.

The more interesting part is before the while loop because the fix is related to protecting the gp_file_name_reduce method:

Let’s see what is this reduce method for:

So, in short, it’s for simplifying path references by when it makes sense.

Based on the commit comments it seems the exploitation is related to a specially crafted path that is passed for validation to gp_validate_path_len. When the validation process does not reject the path or return something unexpected, and the file is opened somehow, the injected code is triggered. This was my hypothesis, at least.

Reproducing the vulnerability

PS and EPS files

My next questions were:

  1. How to trigger file write when opening an EPS file?
  2. How does a file write lead to command execution?

Let’s see what an EPS file is first. EPS stands for “Encapsulated PostScript.” It is a file format that encapsulates both vector and raster graphics, along with their textual descriptions, in a self-contained document. EPS files are primarily used for printing, as they contain instructions that can be interpreted by a PostScript interpreter to render images and graphics with high precision and quality.

After some research I had not much success with the first question so dig deeper with Postscript language and finally, I found one of the most important puzzles for exploitation: it’s possible to read from a file in PostScript. The syntax is:

(/path/to/your/file.txt) (r) file

OK, so what happens if I change the “path” to an existing image and open the PS file with Ghostscript? I created a PS file (test.ps) with some content:

%!PS% 
Translate the coordinate system
10 830 translate
% Scale the coordinate system by a factor of 175 in both X and Y directions
175 175 scale
% Define the dimensions and transformation matrix for the image
705
218
8
[705 0 0 -218 0 0]
% Embed the JPEG image "vsociety.jpg" and apply the DCTDecode filter
(vsociety.jpg) (r) file
/DCTDecode filter
% Specify that the image will be displayed using the RGB color space
false
% Specify the number of color components for the image (3 for RGB)
3
% Display the image using the specified settings
colorimage
% Display the page
showpage

When trying to open it with our Ghostscript executable and we will see that it has some issues related to the permissions. From version 9.50 we also have to add the -dNOSAFER option when dealing with file I/O operations. Make sure you have a picture with the name vsociety.jpg in the same folder. With that, the result is:

It just renders a file that was referred to in the PS. My next question was: does it also works in an EPS file? Let’s see!

Create a file (test.eps) with similar content:

%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 300 200
%%Title: EPS with Referenced JPG
%%Creator: vsociety
% Translate the coordinate system
10 830 translate
% Scale the coordinate system by a factor of 175 in both X and Y directions
175 175 scale
% Define the dimensions and transformation matrix for the image
705
218
8
[705 0 0 -218 0 0]
% Embed the JPEG image "vsociety.jpg" and apply the DCTDecode filter
(vsociety.jpg) (r) file
/DCTDecode filter
% Specify that the image will be displayed using the RGB color space
false
% Specify the number of color components for the image (3 for RGB)
3
% Display the image using the specified settings
colorimage
% Display the page
showpage

And then run it with Ghostscript:

Oh yeah.

Command injection

Based on what we already know in the static analysis section we still have to change two things in this code:

- the name of the file

- the permission

The name should start with %pipe% as we know. And also, the syntax when making some operation in Ghostscript is %pipe%app where the app is something we want to pipe the output into. Let's put these together and change the related code part to this in the EPS file to trigger a calculator under Windows:

(%pipe%calc) (w) file

Copy test.eps into a new file (attack.eps) with this changed line of code.

Now let’s see what happens if we run the code. Use this command:

.\gswin64c.exe -dNOSAFER attack.eps

And boom — we have command execution via opening an EPS.

Debugging

Let’s see what we have under the hood!

Open Project / ghostscript Properties and select Debugging tab.

Set the attack.eps as an argument and click on Apply (sorry for the foreign labels here).

We saw before that the modified code is in base/gpmisc.c and affects the gp_validate_path_len method.

The gp_validate_path_len method validates a given file path and its associated permissions.

Parameters:

  • const gs_memory_t *mem: A memory context for memory allocation.
  • const char *path: The file path to be validated.
  • const uint len: The length of the file path.
  • const char *mode: A string indicating the desired file access mode (e.g., "r" for read, "w" for write, etc.).

Return Value: Returns an integer code indicating the result of the validation process.

Set up a breakpoint there at first because we can assume that it will be hit. We are interested in file writing so put one breakpoint to line 1096 where the validation happens:

The validate function is responsible for comparing a given file path with a set of control patterns and determining whether the path is permitted according to the specified control type (reading, writing, or control). Let's dive into the function and understand its behavior:

  1. Input parameters:
  • const gs_memory_t *mem: Memory context for memory allocation.
  • const char *path: The file path to be validated.
  • gs_path_control_t type: The type of control to be enforced (gs_permit_file_reading, gs_permit_file_writing, or gs_permit_file_control).

2. Path control set retrieval:

  • Depending on the type of control requested, the function retrieves the appropriate control set from the gs_lib_ctx_core_t structure. This structure seems to contain permission control information for different file access modes.

3. Pattern Matching Loop:

  • The function then iterates through the entries in the control set.
  • For each entry, it compares the provided path with the pattern associated with the entry.

4. Pattern matching logic:

  • The pattern-matching process involves comparing characters from both the path and the pattern.
  • Wildcards are supported: * is used as a wildcard character.
  • * at the end of the pattern indicates that the rest of the path (or subdirectories) can match any characters.
  • Multiple consecutive * characters are treated as a single *.
  • The matching sequence stops when a non-matching character is encountered or when the end of either the path or the pattern is reached.
  • Some additional logic handles the handling of explicit escapes (\) and handling of mixed directory separators (/ and \) on Windows.

5. Matching result:

  • If a match is found, the function returns the associated flags from the control entry, indicating that the specified access is permitted.
  • If no match is found among the control entries, the function returns gs_error_invalidfileaccess, indicating that access is not permitted.

6. Behavior for different control types:

  • The function uses the provided type parameter to determine which control set to use (permit_reading, permit_writing, or permit_control).

7. Return Values:

  • If a valid match is found, the function returns the permission flags associated with that control entry.
  • If no valid match is found among the control entries, the function returns gs_error_invalidfileaccess.

In summary, the validate function plays a role in enforcing file access controls based on predefined patterns and control sets. It compares the provided file path against these patterns, considering wildcard characters and directory separators, and determines whether the specified type of access is allowed or not.

Click on Local Windows Debugger under the menu bar to start the debug session:

The breakpoint is hit almost immediately: Ghostscript tries to open %pipe%calc. Hit F11 to "step into" the validate method. the buffer variable here stores the "reduced" path.

After some steps (hitting F10 in VS) we can see that the file writing permit is validated in the validate method and the path equal to "%pipe%calc".

However, it returns with the code “2" that seems like no error there and nothing else happens.

After a couple of steps, it turns out that the whole process ends up with no calculator pop-up. The reason is simple: we forgot the -dNOSAFER argument that is needed from a specific version in case I/O operations in Ghostscript. Let's modify the command arguments and restart the debug process. Open the Project / Ghostscript Properties menu and select Debugging tab.

When we run the app again, we can see that the calculator is triggered but there was no interruption due to breakpoints hit.

To find out what’s happening, let’s set a so-called conditional breakpoint to gp_validate_path_len method. A condition is an expression or statement that is evaluated during runtime, and the breakpoint will only pause the program's execution and trigger when the condition evaluates to true. Without a condition, the execution stops too many times because there is a path-checking mechanism in Ghostscript with several non-existing paths but we want to stop the program execution only if "%pipe%calc" is in the path.

Set it with the following condition: strstr(path, "%pipe%calc")!=0. Also, uncheck the "Continue code execution" checkbox and restart the debug process again! This will pause the execution if the value of the path variable contains the given string. (The other breakpoint can be deleted.)

Start the debugging again!

The program execution pauses when it tries to validate the “%pipe%calc" path you can see below.

Another thing to notice is in the call stack: the gp_validate_path method is called from the "pipe_fopen" method so it's already "recognized" that our "file path" is a pipe.

Hit F10 for a couple of times till the execution is at line 1055 where the app checks the mem->gs_lib_ctx for validity and the path_control_active flag:

/* mem->gs_lib_ctx can be NULL when we're called from mkromfs */
if (mem->gs_lib_ctx == NULL ||
mem->gs_lib_ctx->core->path_control_active == 0)
return 0;

The execution does not reach the validate part where the previous breakpoint was in this method because it returns with 0 to the pipe_fopen method.

Let’s see this pipe_fopen function then that is used to open a file or a pipe using the appropriate method based on the provided input parameters.

Parameters:

  • gx_io_device *iodev: An I/O device structure that specifies the device type.
  • const char *fname: The name of the file or command to open.
  • const char *access: The access mode for opening the file or pipe.
  • gp_file **pfile: A pointer to a pointer that will be set to the opened file handle.
  • char *rfname: A buffer where the resolved file name will be stored (if needed).
  • uint rnamelen: Length of the buffer for resolved file name.
  • gs_memory_t *mem: Memory context for memory allocation.

Functionality:

  • It checks whether the Ghostscript library is compiled with file system support.
  • It constructs the full file name by combining the I/O device name and the provided file name.
  • It uses the gp_validate_path function to validate the constructed file name and access mode.
  • It then attempts to open the file or pipe using various file system implementations based on the available list of file systems.
  • If a matching file system implementation is found, it calls the appropriate open_pipe function associated with that file system.
  • If successful, the opened file handle is stored in pfile.

Then press F11 to step into fs_file_open_pipe called from pipe_fopen (line 111 in gdevpipe.c). This function is responsible for opening a pipe for reading or writing in the Ghostscript library.

Parameters:

  • const gs_memory_t *mem: Memory context for memory allocation.
  • void *secret: A pointer to secret information (specific to file system implementation).
  • const char *fname: The name of the file (or command as we will see) to open.
  • char *rfname: A pointer to a buffer where the resolved file name will be stored (if needed).
  • const char *mode: The mode in which to open the pipe (e.g., "r" for reading, "w" for writing).
  • gp_file **file: A pointer to a pointer that will be set to the opened file handle.

Functionality:

  • The function allocates memory for a gp_file structure using gp_file_FILE_alloc.
  • It uses the popen function (which is used to open a pipe to a command) to open the pipe. The popen method is a function in many programming languages, including C, C++, and Python, that is used to open a pipe to or from a command-line process. It allows your program to interact with the standard input or output of another command-line program. The name "popen" stands for "pipe open".
  • If the pipe is successfully opened, it associates the gp_file structure with the pipe handle using gp_file_FILE_set.
  • The resolved file name is stored in rfname if it is provided.
  • The function returns an error code if any step fails.

The magic happens in line 55 where the popen method opens the content of the fname variable that is "calc". In this case, "calc" refers to the built-in Windows Calculator application. When a command like "calc" is passed to popen, it will attempt to execute the command just as if you had entered it in the command prompt. Here's what will happen:

After one more step, the calculator appears.

So the full exploitation path is slightly different than what I thought at first glance but all in all its because of the file name path “reducing” flaw that leads to treating a file name containing “%pipe%" as a pipe instead of a filename.

That call path is:

The relevant part of the call stack for a better understanding of how data flows into the code execution path:

  1. zfile ->
  2. zopen_file: open a file specified by a parsed file name (which may be only a device) ->
  3. iodev_os_open_file ->
  4. file_open_stream: opens a file stream (optionally on an OS file). Gets iodev variable ->
  5. pipe_fopen: opens a pipe. Gets fname and iodev
  6. -> gp_validate_path -> gp_validate_path_len: gets f that consist of iodev->dname and fname (%pipe% and calc). Validates the value of f as a valid path.

2. -> fs.open_pipe: gets the fname variable (value: "calc") and opens it --> command execution

Patch diffing

See the official path on git here or here.

The patched code includes comments that explain the handling of special cases and why they’re treated differently.

As you can see the new code wraps the call to the function “gp_file_name_reduce()” inside a new conditional block and allows the execution only if the path does not start with the string “%pipe” (or, in the final patch, its syntactical equivalent "|").

Let’s go through the differences and improvements made in the patched code for enhancing security:

  1. Handling of special cases: In the patched code, there’s a specific check for special cases where paths starting with "|" or "%pipe" are handled differently. These paths are not subjected to the usual path reduction mechanism to avoid unexpected results.
  2. Path buffer allocation: The patched code allocates the path buffer differently based on whether the path is a special case or not. For special cases, the buffer is allocated directly with the length of the input path. For normal cases, the buffer size is calculated as rlen + prefix_len, taking into account the reduced path length and any additional prefixes.

3. Buffer manipulation: The patched code also modifies how the buffer is manipulated for special cases. Instead of performing path reduction and copying, the buffer is directly copied from the input path for special cases. This might help avoid unnecessary manipulation of potentially unsafe path strings.

Overall, the patched code appears to be an attempt to mitigate potential vulnerabilities and improve the security of the path validation process.

Mitigation

Recommendations:

  • Make sure your computers have the latest security updates. Upgrade to the latest version or apply security patches to your current OS version, for example in Debian bullseye, update from version 9.53.3~dfsg-7+deb11u4 to 9.53.3~dfsg-7+deb11u5 as detailed here.
  • Update Ghostscript to at least 10.01.2.
  • Some programs might use Ghostscript without you knowing. If your programs can show PDF or EPS files, it’s a good idea to check if they use Ghostscript and update them when the software maker releases fixes.
  • Keep all your computers up to date with the latest fixes. This helps protect against known problems that hackers might try to take advantage of.

Final thoughts

In wrapping up, analyzing this big chunk of code for potential vulnerabilities has been quite a journey. It’s my first time digging into such a massive codebase for security issues, and I must say, it’s been really exciting and a bit nerve-wracking too.

The best part? Well, that moment when I found the problem and a calculator popped up out of the blue — it was like all the hard work suddenly paid off big time.

Additionally, this whole experience taught me a bunch of new stuff. I got the hang of debugging using tools like Visual Studio, and I also got to learn about those EPS and PS files, which was pretty cool.

All in all, this CVE adventure wasn’t just about finding bugs. It was a real eye-opener, showing me how diving into challenges head-on can be super rewarding.

Resources

Join vsociety: https://vsociety.io/

Checkout our discord: https://discord.gg/sHJtMteYHQ

--

--

vsociety
vsociety

Written by vsociety

vsociety is a community centered around vulnerability research

No responses yet