Tuesday, October 23, 2012

Exploiting eval() function in Python

          I came across a pretty interesting function in python yesterday while coding. It is the eval() function. Basically it takes a string which has a valid python expression, and evaluates it as a regular python code. The risk with this function, if the user manages to enter custom crafted string into this function, it has capability to execute shell commands. Once a remote attacker manages to enter shell commands into your python file hosted on web server, it won't be long before you have to run to the office at midnight because you got a call that web server is compromised.

Here's a link to the usage of eval() function in official python documentation.
          http://docs.python.org/library/functions.html#eval

Using eval function:
The following example demonstrates how eval() works:
         
So it simply takes a string as expression and evaluates it. How can we exploit it to get shell codes executed? There are few ways of doing so.

1. Consider running the following code:

          >>> eval("os.getcwd()")


     This will give your present working directory. You can execute other functions of OS module too, like this one.

         >>>eval("os.system('clear')")
     
     This call will clear the console window and will just display a '0' which i guess is the return value of that function. It works on linux only, windows users, try replacing 'clear' with 'CLS'.
Okay that's there, but how can a potential attacker exploit it? well, what if you replace os.system('clear') with something like os.system('rm -rf /') ?  You get the point right... :D
p.s. please don't execute this one unless you want a clean slate!

2. There's another interesting module in python called as 'subprocess'. 
     http://docs.python.org/library/subprocess.html
The subprocess module allows spanning of new processes and connecting their i/o through pipes. As the official documentation puts it, it intends to replace older modules and functions including os.system.
with the getoutput() function you can execute commands and get their output as follows:

          >>> import subprocess
          >>> subprocess.getoutput('ls')
          >>>eval("subprocess.getoutput('pwd')")

Again, ls or pwd can be replaced by more harmful commands, something like, "rm /etc/passwd" or "rm /any/file/present" depending on your current user privileges.

          >>>eval("subprocess.getoutput('rm /any/file/present')")

Wait a minute!
This only works if you have the OS module or subprocess module imported right? The program you're trying to exploit must have these modules imported in advance. Try running the command

          >>> eval("os.getcwd()")

without importing the OS module. It will give a NameError saying that name 'os' is not defined.
Well, there is a way around this...

There's a global __import__() function in python. It accepts a module name and imports it.
Hence,
__import__('os')
will import the os module and will return a reference to this module. Through this, we can execute functions in the OS module with malicious parameters. The crafted eval() function will look like this:

         >>> eval('__import__("subprocess").getoutput("ls")')


That's all from the attacker point of view, here's how we can 'try' to minimize the risk associated with eval() function.
If you've seen the official documentation of eval(), there are second and third parameters that this function accepts. They are the global and local namespaces that this function accepts for evaluating the expression. The global namespace must be a dictionary while local namespace can be any mapping object.


This example demonstrates how global namespace works. In the first attempt, our global and local namespaces are blank, indicated by {},{} parameters. (by the way, these namespaces are dictionaries that map its index element to its value) Hence, there is no reference of the 'num' object that python can find. Hence it raises an exception saying that 'num' is not defined.
In the second attempt, we set the reference of "num" to the num variable that we have set to 10. Hence now the python can evaluate num*num and return 100.

So, does setting global and local namespaces to blank ({},{}) solve our problem of arbitrary command execution?

               >>> eval('__import__("os").getcwd()',{},{})

It doesn't... because __import__ is a part of "__builtins__" pseudo module. This module and its functions are available to the python program unless you manually restrict it. Now, in the global namespace, you will map "__builtins__" to something blank, empty, a None object maybe. This will prevent the __import__ function (or any other function from __builtins__ ) getting called. Global namespace will look like, {"__builtins__":None} or {"__builtins__":{}}

Hence, now the function,

     >>> eval('__import__("os").getcwd()',{"__builtins__":{}},{})

Will fail because it can't find any reference to __import__ and os either.

This will mitigate most of the security risks, but is still not a foolproof solution. There are ways to bypass the __builtins__:None too, but that is out of the scope of this article.

There are other ways of securing your eval() function too, like removing all the double underscores ("__") from a string before it is passed over to the eval function. This should prevent most of the attacks as of now unless someone comes up with a new method to bypass this. :D

astring = "This string has __double__ underscores"
astring = astring.replace("__","")

The above string can now be passed to the eval function.

One more way, use the literal_eval from the module "ast". ast.literal_eval is a much more secure way to evaluate python strings than the generic eval() function. More documentation about ast module can be found here.

http://docs.python.org/library/ast.html

There are other such dangerous functions in python too, like exec() or file exec and again, there are ways to exploit these and also ways to thwart possible attacks. Input validation plays important role here if you are deploying these on web server or any other public server.


Thanks for reading! :)