Thoughts on vulnerabilities caused by a Python command
0x00 cause
Recently, when testing a project, you have no intention of discovering that you can directly run a Python command on the client machine to execute the Python script on the server. Therefore, the following is an in-depth test.
0x01 analysis
In many cases, we often need to use the Python-c exec method to execute Python scripts or commands on the remote server on the client.
In this case, because the command is run on the client, this will inevitably cause the Python script on the remote server to run in the client's memory in a certain form, if we can obtain and restore the code, this also causes leakage of the server source code to some extent.
To verify this risk, I created a simple Demo based on a real case:
First, a Python script pyOrign is created on the server. py to simulate the service code and then use the compile method. py is compiled into a code object in exec mode and uses marshal. the dump method is serialized into a binary file pyCode, Which is saved on the server for remote calls by the client, and a test script is created on the server. py, which is used to call and deserialize the server-side binary file. pyCode is the code object that can be executed by the exec method.
PyOrign. py file:
#!python#!/usr/bin/env python import randomimport base64 class Test: x='' y=''def __init__(self, a, b):self.x = aself.y = bprint "Initiation..., I'm from module Test"def add(self):print 'a =',self.xprint 'b =',self.y c = self.x+self.yprint 'sum =', cif __name__ == '__main__':print "\n[+] I'm the second .py script!" a = Test(1,2)a.add()
Test. py file:
#!python#!/usr/bin/env pythonimport imp ifimp.get_magic() != '\x03\xf3\r\n':print "Please update to Python 2.7.10 (http://www.python.org/download/)"exit() importurllib, marshal, zlib, time, re, sysprint "[+] Hello, I'm the first .py script!"_S = "http"_B = "10.66.110.151"execmarshal.loads(urllib.urlopen('%s://%s/mystatic/pyCode' % (_S, _B)).read())
Next, let's demonstrate the effect. First, run the following command on the client:
#!bashpython -c "exec(__import__('urllib2').urlopen('http://10.66.110.151/test/').read())"
The running result is as follows:
After a brief analysis of this process, it is not difficult to find that the above Command actually happens after it is executed as follows:
First, use urllib2's urlopen method to read the command code on the remote server.
Then judge whether the python version on the client is 2.7.10. If yes, execute the following code to continue obtaining the executable code on the remote server: exec marshal. loads (urllib. urlopen ('HTTP: // 10.66.110.151/mystatic/pycode '). read ())
Then, the urllib urlopen method is used to read executable code on the remote server:
Finally, the exec method executes the deserialization code object of the marshal. loads method on the client.
Careful friends may have discovered that in step 3, we did not get the source code for exec execution as in step 1, but a codeobject object. So we can't help but think about whether we can restore this code object to a real Python source code? If yes, does it mean that the source code on the server has this high risk of leakage?
We know that the exec statement is used to execute Python statements stored in strings or files. This can be either a Python statement or a code object in exec mode after compile compilation. So here, you can't help but think about whether the obtained code object is a code object in exec mode after compile compiling by the Python script on the server? If yes, as long as we can construct the pyc file after the original script is compiled, we can use the pyc file to further restore the original py file of the script.
Next, let's take a look at how to use the known codeobject object to construct a compiled pyc file.
First, let's analyze the composition of the pyc file. A complete pyc file consists of the following parts:
Four-byte Magic int (Magic NUMBER) indicates the four-byte int of pyc version information, which is the pyc generation time. If the time difference from the py file, the serialized PyCodeObject object will be regenerated.
So, do we already have these parts. The first is the four-byte Magic int, Which is returned to Step 1 during the above analysis. We can see the following code:
#!pythonimport impifimp.get_magic() != '\x03\xf3\r\n':print "Please update to Python 2.7.10 (http://www.python.org/download/)"exit()
Here, the Code uses Magic int to determine the Python version information on the client host. Needless to say, Here Magic int Is the value obtained by imp. get_magic.
The next step is the four-byte pyc timestamp. After my tests, we found that the timestamp can be any four-byte int that conforms to the format.
Finally, the serialized PyCodeObject object. Do we have this? That's right. The codeobject we read in step 3 is the PyCodeObject object.
Since we have constructed all three components of pyc, let's try to construct this pyc file. According to speculation, the remote server should compile the original script file using the compile method, so we will use the same method to construct it.
Here we use the compile method of the library file py_compile. The specific code implementation is as follows:
#!python"""Routine to "compile" a .py file to a .pyc (or .pyo) file.This module has intimate knowledge of the format of .pyc files."""import __builtin__import impimport marshalimportosimport sysimporttracebackMAGIC = imp.get_magic()__all__ = ["compile", "main", "PyCompileError"]classPyCompileError(Exception): """Exception raised when an error occurs while attempting tocompile the file. To raise this exception, useraisePyCompileError(exc_type,exc_value,file[,msg])whereexc_type: exception type to be used in error messagetype name can be accesses as class variable 'exc_type_name'exc_value: exception value to be used in error messagecan be accesses as class variable 'exc_value'file: name of file being compiled to be used in error messagecan be accesses as class variable 'file'msg: string message to be written as error message If no value is given, a default exception message will be given,consistent with 'standard' py_compile output.message (or default) can be accesses as class variable 'msg' """def __init__(self, exc_type, exc_value, file, msg=''):exc_type_name = exc_type.__name__ifexc_type is SyntaxError:tbtext = ''.join(traceback.format_exception_only(exc_type, exc_value))errmsg = tbtext.replace('File "
"', 'File "%s"' % file)else:errmsg = "Sorry: %s: %s" % (exc_type_name,exc_value) Exception.__init__(self,msg or errmsg,exc_type_name,exc_value,file)self.exc_type_name = exc_type_nameself.exc_value = exc_valueself.file = file self.msg = msg or errmsgdef __str__(self):return self.msgdefwr_long(f, x): """Internal; write a 32-bit int to a file in little-endian order."""f.write(chr( x & 0xff))f.write(chr((x >> 8) & 0xff))f.write(chr((x >> 16) & 0xff))f.write(chr((x >> 24) & 0xff))def compile(file, cfile=None, dfile=None, doraise=False): """Byte-compile one Python source file to Python bytecode. Arguments:file: source filenamecfile: target filename; defaults to source with 'c' or 'o' appended ('c' normally, 'o' in optimizing mode, giving .pyc or .pyo)dfile: purported filename; defaults to source (this is the filenamethat will show up in error messages)doraise: flag indicating whether or not an exception should beraised when a compile error is found. If an exceptionoccurs and this flag is set to False, a stringindicating the nature of the exception will be printed,and the function will return to the caller. If anexception occurs and this flag is set to True, aPyCompileError exception will be raised. Note that it isn't necessary to byte-compile Python modules forexecution efficiency -- Python itself byte-compiles a module whenit is loaded, and if it can, writes out the bytecode to thecorresponding .pyc (or .pyo) file. However, if a Python installation is shared between users, it is agood idea to byte-compile all modules upon installation, sinceother users may not be able to write in the source directories,and thus they won't be able to write the .pyc/.pyo file, and thenthey would be byte-compiling every module each time it is loaded. This can slow down program start-up considerably. See compileall.py for a script/module that uses this module tobyte-compile all installed files (or all files in selecteddirectories). """with open(file, 'U') as f:try:timestamp = long(os.fstat(f.fileno()).st_mtime)exceptAttributeError:timestamp = long(os.stat(file).st_mtime)codestring = f.read()try:codeobject = __builtin__.compile(codestring, dfile or file,'exec')exceptException,err:py_exc = PyCompileError(err.__class__, err, dfile or file)ifdoraise:raisepy_excelse:sys.stderr.write(py_exc.msg + '\n')returnifcfile is None:cfile = file + (__debug__ and 'c' or 'o')with open(cfile, 'wb') as fc:fc.write('\0\0\0\0')wr_long(fc, timestamp)marshal.dump(codeobject, fc)fc.flush()fc.seek(0, 0)fc.write(MAGIC)def main(args=None): """Compile several source files. The files named in 'args' (or on the command line, if 'args' isnot specified) are compiled and the resulting bytecode is cachedin the normal manner. This function does not search a directorystructure to locate source files; it only compiles files namedexplicitly. If '-' is the only parameter in args, the list offiles is taken from standard input. """ifargs is None:args = sys.argv[1:]rv = 0ifargs == ['-']:while True:filename = sys.stdin.readline()if not filename:breakfilename = filename.rstrip('\n')try:compile(filename, doraise=True)exceptPyCompileError as error:rv = 1sys.stderr.write("%s\n" % error.msg)exceptIOError as error:rv = 1sys.stderr.write("%s\n" % error)else:for filename in args:try:compile(filename, doraise=True)exceptPyCompileError as error: # return value to indicate at least one failurerv = 1sys.stderr.write(error.msg)returnrvif __name__ == "__main__":sys.exit(main())
In the code above, we can see that the compile method first generates a Magic int using imp. get_magic:
#!pythonMAGIC = imp.get_magic()
The timestamp is generated based on the Creation Time Of The py file:
#!pythontimestamp = long(os.fstat(f.fileno()).st_mtime
Finally, the code object in exec mode is generated using the _ builtin _. compile method, and the codeobject is written to the pyc file using the marshal. dump method.
#!pythoncodeobject = __builtin__.compile(codestring, dfile or file,'exec')
After understanding the principle, we can use the following script to construct the pyc file:
#! Python "Routine to" compile ". py file to. pyc (or. pyo) file. this module has intimate knowledge of the format. pyc files. "import _ builtin _ import impimport export alimportosimport sysimporttracebackimportzlibimporturllibMAGIC = imp. get_magic () # The magic number _ all _ = ["compile", "main", "PyCompileError"] classPyCompileError (Exception) generated based on the Python version information ): "Exception raised when an error occurs while attempting tocompile the file. to raise this exception, useraisePyCompileError (exc_type, exc_value, file [, msg]) whereexc_type: exception type to be used in error messagetype name can be accesses as class variable 'exc _ type_name 'exc _ value: exception value to be used in error messagecan be accesses as class variable 'exc _ value' file: name of file being compiled to be used in error messagecan be accesses as class variable 'file' msg: string message to be written as error message If no value is given, a default exception message will be given, consistent with 'standard' py_compile output. message (or default) can be accesses as class variable 'msg '"" def _ init _ (self, exc_type, exc_value, file, msg = ''): exc_type_name = exc_type. _ name _ ifexc_type is SyntaxError: tbtext = ''. join (traceback. format_exception_only (exc_type, exc_value) errmsg = tbtext. replace ('file"
"', 'File" % s "' % File) else: errmsg =" Sorry: % s "% (exc_type_name, exc_value) Exception. _ init _ (self, msg or errmsg, exc_type_name, exc_value, file) self. exc_type_name = exc_type_nameself.exc_value = exc_valueself.file = file self. msg = msg or errmsgdef _ str _ (self): return self. msgdefwr_long (f, x): "" Internal; write a 32-bit int to a file in little-endian order. "f. write (chr (x & 0xff) f. write (chr (x> 8) & 0xff) f. write (chr (x> 16) & 0xff) f. write (chr (x> 24) & 0xff) def compile (file, cfile = None, dfile = None, doraise = False ): "" Byte-compile one Python source file to Python bytecode. arguments: file: source filenamecfile: target filename; defaults to source with 'C' or 'O' appended ('C' normally, 'O' in optimizing mode, giving. pyc or. pyo) dfile: purported filename; defaults to source (this is the filenamethat will show up in error messages) doraise: flag indicating whether or not an exception shocould beraised when a compile error is found. if an exceptionoccurs and this flag is set to False, a stringindicating the nature of the exception will be printed, and the function will return to the caller. if anexception occurs and this flag is set to True, aPyCompileError exception will be raised. note that it isn' t necessary to byte-compile Python modules forexecution efficiency -- Python itself byte-compiles a module whenit is loaded, and if it can, writes out the bytecode to thecorresponding. pyc (or. pyo) file. however, if a Python installation is shared between users, it is agood idea to byte-compile all modules upon installation, sinceother users may not be able to write in the source directories, and thus they won't be able to write. pyc /. pyo file, and thenthey wocould be byte-compiling every module each time it is loaded. this can slow down program start-up considerably. see compileall. py for a script/module that uses this module tobyte-compile all installed files (or all files in selecteddirectories ). "timestamp = long (1449234682) # It Can Be A randomly generated timestamp. try: codeobject = marshal. loads (urllib. urlopen (' http://10.66.110.151/mystatic/pyCode '). Read () # deserialize and obtain the code object jsontexception, err: py_exc = PyCompileError (err. _ class __, err, dfile or file) ifdoraise: raisepy_excelse: sys. stderr. write (py_exc.msg + '\ n') returnifcfile is None: cfile = file + (_ debug _ and 'C' or 'O') with open (cfile, 'wb ') as fc: fc. write ('\ 0 \ 0 \ 0 \ 0') wr_long (fc, timestamp) marshal. dump (codeobject, fc) # Write the serialized code object to the python file fc. flush () fc. seek (0, 0) fc. write (MAGIC) def main (args = None): "" Compile several source files. the files named in 'args' (or on the command line, if 'args' isnot specified) are compiled and the resulting bytecode is cachedin the normal manner. this function does not search a directorystructure to locate source files; it only compiles files namedexplicitly. if '-' is the only parameter in args, the list offiles is taken from standard input. "" ifargs is None: args = sys. argv [1:] rv = 0 ifargs = ['-']: while True: filename = sys. stdin. readline () if not filename: breakfilename = filename. rstrip ('\ n') try: compile (filename, doraise = True) effectpycompileerror as error: rv = 1sys. stderr. write ("% s \ n" % error. msg) exceptIOError as error: rv = 1sys. stderr. write ("% s \ n" % error) else: for filename in args: try: compile (filename, doraise = True) handle tpycompileerror as error: # return value to indicate at least one failurerv = 1sys. stderr. write (error. msg) returnrvif _ name _ = "_ main _": compile ('pyorigin. py ')
Save the script as pyOrigin_compile.py and run it in Python 2.7.10 to construct the pyOrigin. pyc file:
Then, use uncompyle2 to restore pyOrigin. pyc to pyOrigin. py. So far, we have successfully restored the original Python script.
0x03 Summary
Analysis ideas:
According to the Python-c exec command, the code object after execution is obtained. It is assumed that it is the code object that can be executed by exec after compilation by the compile method. The py_compile compile method is used to compile the py file. it is the principle of the pyc file and constructs the pyc File Based on the obtained code object to use uncompyle2 to restore the pyc file as a py file, finally, obtain the Python source code to be executed.
The main potential hazard is the leakage of Python source code on the remote server.