In this post I’ll explain how I discovered a sandbox bypass in Ghostscript that allows arbitrary shell command execution when an untrusted PostScript or PDF file is viewed or processed. This bug was reported to Artifex on 12 November as bug 700153. It has since been assigned the CVE ID CVE-2018-19475. This bug is a variant of several vulnerabilities that Google Project Zero member Tavis Ormandy reported in August. I found this vulnerability using our query technology for variant analysis. Using CodeQL, I was able to quickly query the Ghostscript source code to identify variants. The queries used in this post can be run against a Ghostscript snapshot using the CodeQL for Eclipse plugin.

[EDIT]: You can also use our free CodeQL extension for Visual Studio Code. See installation instructions at https://securitylab.github.com/tools/codeql/.

The SAFER mode sandbox in Ghostscript

Ghostscript is an interpreter for PostScript, which is a scripting language for document rendering. As part of its command set, Ghostscript supports a %pipe% syntax, which specifies that the PostScript interpreter should start a new process with any shell command. For example, the following command (on any Linux machine with Ghostscript and xcalc installed) will launch the xcalc calculator:

$ gs
GS>(%pipe%xcalc) (w) file

This, of course, can be a huge security problem when a PostScript file comes from an untrusted source (for example, downloaded online). To mitigate the issue, Ghostscript supports a sandbox mode. If Ghostscript is launched with the -dSAFER option, then the %pipe% syntax will be disabled. The following command will raise a file access error:

$ gs -dSAFER
GS>(%pipe%xcalc) (w) file

Applications such as ImageMagick and Evince (the default PDF viewer in many Linux distributions) use Ghostscript to parse PostScript and PDF files; and as most PostScript and PDF files are often downloaded from untrusted sources, these applications usually use the -dSAFER option when invoking Ghostscript.

The documentation indicates that SAFER mode disables certain dangerous operators, such as deletefile, renamefile, pipe commands (%pipe%) as well as setting a boolean parameter called .LockSafetyParams. The documentation does not say much about the LockSafetyParams, other than that it prevents the modification of “potentially dangerous device parameters such as OutputFile”. One of the important protections offered by LockSafetyParams is that, when LockSafetyParams is set to true, the OutputFile parameter of a device cannot be changed. When the OutputFile parameter is set to a pipe command, Ghostscript will happily execute any shell command:

$ gs
GS>mark /OutputFile (%pipe%xcalc) currentdevice putdeviceprops
GS>showpage

In SAFER mode, the above will give an invalidaccess error as LockSafetyParams is preventing the change of OutputFile. However, as long as an attacker manages to overwrite LockSafetyParams so that OutputFile can be changed in the first line, this will launch the calculator, even in SAFER mode. This makes the LockSafetyParams very valuable when looking to exploit Ghostscript.

Project Zero Bug 1640

The vulnerability I found (CVE-2018-19475) is a variant of the vulnerability reported here, I’ll briefly explain that vulnerability. There are actually a few variants of this bug, filed as 699654, (fixed here), 699714 (fixed here), 699718 (fixed here and here). All these seem to be triggered by a failed PostScript restore operation. When an error occurs in the middle of a restore, the PostScript device is left in an inconsistent state, with LockSafetyParams set to false. For example, this commit and this commit both introduced error-handling code that ensures that LockSafetyParams is reset to its original state. So where is LockSafetyParams set to false in the process of a restore? I can use CodeQL to find out:

from FieldAccess fa
where fa.getTarget().hasName("LockSafetyParams") and
exists(AssignExpr expr | expr.getLValue() = fa and expr.getRValue() instanceof Zero) and
exists(Function f | f.getName().matches("%restore%") and f.calls*(fa.getEnclosingFunction()))
select fa

You can run this query yourself on a Ghostscript source code snapshot database using the CodeQL for Eclipse plugin. The query looks for places where LockSafetyParams is set to false in a function called by a function whose name contains the word “restore”. This gives me one result:

LockSafetyParams

The comments make it quite clear that restore needs to temporarily disable LockSafetyParams to perform a privileged action. This, of course, relies on the developer restoring LockSafetyParams at a later stage. However, by triggering an error during a restore after this point, but before LockSafetyParams is restored to its original value, it is possible to leave the device in a state where LockSafetyParams is set to false. This is essentially what happened with many of the bugs mentioned above.

Another flag, please

The fact that LockSafetyParams is switched off is interesting. I wonder if there are other flags that need to be disabled during a restore for similar reasons? Let’s take a look with CodeQL again:

from FieldAccess fa
where exists(AssignExpr expr | expr.getLValue() = fa and expr.getRValue() instanceof Zero) and
exists(Function f | f.getName().matches("%restore%") and f.calls*(fa.getEnclosingFunction())) and
fa.getTarget().getType().toString() = "bool"
select fa, fa.getTarget(), fa.getLocation()

There are a number of results, but the LockFilePermissions one is particularly interesting:

LockFilePermissions

The comment suggests that it may be something that I am looking for. Let’s check what this flag is used for:

from FieldAccess fa
where fa.getTarget().hasName("LockFilePermissions")
select fa, fa.getEnclosingFunction()

There are only 12 results, but look what I’ve found:

check_file_permissions

Looks like LockFilePermissions is used to check whether a pipe command is allowed as a file name!

Capture the flag

This all looks too easy. LockFilePermissions is set to false at the end of dorestore, which is called by z2restore, which can be called in PostScript using the restore operation. Does it mean that a normal restore will actually set LockFilePermissions to false and the exploit is as simple as?

GS>save restore
GS>(%pipe%xcalc) (w) file

Of course not.

invalidaccess

That would be way too easy, right? So what happened here? Let’s set a breakpoint at the end of check that z2restore to see if LockFilePermissions is switched off.

z2restore

It does look like LockFilePermissions is switched off at the end of z2restore. Only that z2restore is not the end of restore. It turns out that the C code in Ghostscript is only part of Ghostscript. Many higher-level operators are implemented in the PostScript language, and restore is one of them. It is actually implemented in gs_lev2.ps:

/restore {              % <save> restore -
   //restore /userparams .systemvar .setuserparams
} .bind odef

The restore operator available in PostScript actually first calls z2restore, then uses userparams to restore the original settings, including the LockFilePermissions. This all sounds very familiar. If I can now find a way to trigger an error after z2restore is called, but before .setuserparams, then the device will be left with LockFilePermissions set to false.

So what could go wrong there? PostScript is a stack-based language and objects are pushed onto an “operand stack”. For example, when I do

GS>/userparams
GS<1>

In the above, userparams got pushed onto the operand stack and my stack now contains one operand, as indicated by the <1> in the following line. Of course, the operand stack is finite and when it fills up, it’ll throw a stackoverflow error:

stackoverflow

So a simple way to break the restore process is to fill up the stack prior to a restore, so that when it tries to push /userparams to the stack, it fails and throws a stackoverflow error:

restore_error

And then use (%pipe%xcalc) (w) file:

calc

To make it into an actual PostScript file, the stackoverflow error will need to be caught and ignored by using {save restore} stopped {} if instead of just a simple save restore. A fully working version is 4 lines long:

%PS
0 1 300367 {} for
{save restore} stopped {} if
(%pipe%xcalc) (w) file

Note that on most machines, Ghostscript will also be called when opening or processing PDF files. Therefore, it doesn’t matter whether you use the .ps or .pdf extension.

Ghostscript version 9.07, which is shipped with CentOS 7, happens to behave slightly differently: it treats the command as a file name and checks whether it is located within a whitelisted directory. This is a bug as the name of the command used in %pipe% was being treated as a file name, which would most likely have led to any legitimate use of %pipe% to fail. This was fixed sometime after 9.07. As a result, the exploit above will not work on CentOS 7. However, it’s easy to work around this check. I’ve included details in the original report to the Ghostscript project.

Conclusions

In this post, I’ve shown how I used CodeQL to perform variant analysis. By using three simple queries, I quickly identified another weakness that I was able to turn into a -dSAFER sandbox escape remote code execution vulnerability.

Disclosure timeline

Note: Post originally published on LGTM.com on January 14, 2019