diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py index fe421b10d..61e7c41e2 100644 --- a/pwnlib/tubes/ssh.py +++ b/pwnlib/tubes/ssh.py @@ -55,21 +55,6 @@ class ssh_channel(sock): #: Command specified for the constructor process = None - #: Working directory - cwd = None - - #: PID of the process - #: Only valid when instantiated through :meth:`ssh.process` - pid = None - - #: Executable of the procesks - #: Only valid when instantiated through :meth:`ssh.process` - executable = None - - #: Arguments passed to the process - #: Only valid when instantiated through :meth:`ssh.process` - argv = None - def __init__(self, parent, process = None, tty = False, wd = None, env = None, raw = True, *args, **kwargs): super(ssh_channel, self).__init__(*args, **kwargs) @@ -290,40 +275,21 @@ def spawn_process(self, *args, **kwargs): def _close_msg(self): self.info('Closed SSH channel with %s' % self.host) - def getenv(self, variable, **kwargs): - """Retrieve the address of an environment variable in the remote process. - """ - if not hasattr(self, 'argv'): - self.error("Can only call getenv() on ssh_channel objects created with ssh.process") - - argv0 = self.argv[0] - - script = ';'.join(('from ctypes import *', - 'import os', - 'libc = CDLL("libc.so.6")', - 'print os.path.realpath(%r)' % self.executable, - 'print(libc.getenv(%r))' % variable,)) - - try: - with context.local(log_level='error'): - python = self.parent.which('python') - - if not python: - self.error("Python is not installed on the remote system.") +class ssh_process(ssh_channel): + #: Working directory + cwd = None - io = self.parent.process([argv0,'-c', script.strip()], - executable=python, - env=self.env, - **kwargs) - path = io.recvline() - address = int(io.recvline()) + #: PID of the process + #: Only valid when instantiated through :meth:`ssh.process` + pid = None - address -= len(python) - address += len(path) + #: Executable of the procesks + #: Only valid when instantiated through :meth:`ssh.process` + executable = None - return int(address) & context.mask - except: - self.exception("Could not look up environment variable %r" % variable) + #: Arguments passed to the process + #: Only valid when instantiated through :meth:`ssh.process` + argv = None def libs(self): """libs() -> dict @@ -335,9 +301,6 @@ def libs(self): verbatim, which may be different than the actual addresses if ASLR is enabled. """ - if not self.executable: - self.error("Can only use libs() on ssh_channel objects created with ssh.process()") - maps = self.parent.libs(self.executable) maps_raw = self.parent.cat('/proc/%d/maps' % self.pid) @@ -368,6 +331,70 @@ def libc(self): e.address = address return e + @property + def elf(self): + """elf() -> pwnlib.elf.elf.ELF + + Returns an ELF file for the executable that launched the process. + """ + import pwnlib.elf.elf + + libs = self.parent.libs(self.executable) + + for lib in libs: + if self.executable in lib: + return pwnlib.elf.elf.ELF(lib) + + + @property + def corefile(self): + import pwnlib.elf.corefile + + finder = pwnlib.elf.corefile.CorefileFinder(self) + if not finder.core_path: + self.error("Could not find core file for pid %i" % self.pid) + + return pwnlib.elf.corefile.Corefile(finder.core_path) + + def getenv(self, variable, **kwargs): + """Retrieve the address of an environment variable in the remote process. + """ + argv0 = self.argv[0] + + script = ';'.join(('from ctypes import *', + 'import os', + 'libc = CDLL("libc.so.6")', + 'print os.path.realpath(%r)' % self.executable, + 'print(libc.getenv(%r))' % variable,)) + + try: + with context.local(log_level='error'): + python = self.parent.which('python') + + if not python: + self.error("Python is not installed on the remote system.") + + io = self.parent.process([argv0,'-c', script.strip()], + executable=python, + env=self.env, + **kwargs) + path = io.recvline() + address = int(io.recvline()) + + address -= len(python) + address += len(path) + + return int(address) & context.mask + except: + self.exception("Could not look up environment variable %r" % variable) + + def _close_msg(self): + self.info('Stopped remote process %r on %s (pid %i)' \ + % (os.path.basename(self.executable), + self.host, + self.pid)) + + class ssh_connecter(sock): def __init__(self, parent, host, port, *a, **kw): super(ssh_connecter, self).__init__(*a, **kw) @@ -519,6 +546,8 @@ def __init__(self, user, host, port = 22, password = None, key = None, # Deferred attributes self._platform_info = {} + self._aslr = None + self._aslr_ulimit = None misc.mkdir_p(self._cachedir) @@ -823,11 +852,11 @@ def func(): pass self.error("preexec_fn cannot be a lambda") func_src = inspect.getsource(func).strip() - setuid = setuid if setuid is None else bool(setuid) + setuid = True if setuid is None else bool(setuid) script = r""" #!/usr/bin/env python2 -import os, sys, ctypes, resource, platform +import os, sys, ctypes, resource, platform, stat from collections import OrderedDict exe = %(executable)r argv = %(argv)r @@ -856,7 +885,7 @@ def is_exe(path): sys.stderr.write("{} is not executable or does not exist in $PATH: {}".format(exe,PATH)) sys.exit(-1) -if %(setuid)r is False: +if not %(setuid)r: PR_SET_NO_NEW_PRIVS = 38 result = ctypes.CDLL('libc.so.6').prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) @@ -872,10 +901,25 @@ def is_exe(path): except Exception: pass +# Determine what UID the process will execute as +# This is used for locating apport core dumps +suid = os.getuid() +sgid = os.getgid() +st = os.stat(exe) +if %(setuid)r: + if (st.st_mode & stat.S_ISUID): + suid = st.st_uid + if (st.st_mode & stat.S_ISGID): + sgid = st.st_gid + if sys.argv[-1] == 'check': sys.stdout.write("1\n") sys.stdout.write(str(os.getpid()) + "\n") - sys.stdout.write(exe + '\x00') + sys.stdout.write(str(os.getuid()) + "\n") + sys.stdout.write(str(os.getgid()) + "\n") + sys.stdout.write(str(suid) + "\n") + sys.stdout.write(str(sgid) + "\n") + sys.stdout.write(os.path.realpath(exe) + '\x00') sys.stdout.flush() for fd, newfd in {0: %(stdin)r, 1: %(stdout)r, 2:%(stderr)r}.items(): @@ -902,7 +946,10 @@ def is_exe(path): pass # Assume that the user would prefer to have core dumps. -resource.setrlimit(resource.RLIMIT_CORE, (-1, -1)) +try: + resource.setrlimit(resource.RLIMIT_CORE, (-1, -1)) +except Exception: + pass %(func_src)s apply(%(func_name)s, %(func_args)r) @@ -923,25 +970,33 @@ def is_exe(path): self.upload_data(script, tmpfile) return tmpfile - execve_repr = "execve(%r, %s, %s)" % (executable, - argv, - 'os.environ' - if (env in (None, os.environ)) - else env) - - # Avoid spamming the screen - if context.log_level >= logging.INFO and len(execve_repr) > 512: - execve_repr = execve_repr[:512] + '...' + if self.isEnabledFor(logging.DEBUG): + execve_repr = "execve(%r, %s, %s)" % (executable, + argv, + 'os.environ' + if (env in (None, os.environ)) + else env) + # Avoid spamming the screen + if self.isEnabledFor(logging.DEBUG) and len(execve_repr) > 512: + execve_repr = execve_repr[:512] + '...' + else: + execve_repr = repr(executable) - with self.progress('Opening new channel: %s' % execve_repr) as h: + msg = 'Starting remote process %s on %s' % (execve_repr, self.host) - if not aslr: - self.warn_once("ASLR is disabled!") + with self.progress(msg) as h: script = sh_string.sh_command_with('for py in python2.7 python2 python; do test -x "$(which $py 2>&1)" && exec $py -c %s check; done; echo 2', script) with context.local(log_level='error'): - python = self.run(script, raw=raw) - result = safeeval.const(python.recvline()) + python = ssh_process(self, script, tty=True, raw=True, level=self.level, timeout=self.timeout) + + try: + result = safeeval.const(python.recvline()) + except Exception: + h.failure("Process creation failed") + self.warn_once('Could not find a Python2 interpreter on %s\n' % self.host \ + + "Use ssh.run() instead of ssh.process()") + return None # If an error occurred, try to grab as much output # as we can. @@ -958,9 +1013,28 @@ def is_exe(path): h.failure("something bad happened:\n%s" % error_message) python.pid = safeeval.const(python.recvline()) + python.uid = safeeval.const(python.recvline()) + python.gid = safeeval.const(python.recvline()) + python.suid = safeeval.const(python.recvline()) + python.sgid = safeeval.const(python.recvline()) python.argv = argv python.executable = python.recvuntil('\x00')[:-1] + h.success('pid %i' % python.pid) + + if aslr == False and setuid and (python.uid != python.suid or python.gid != python.sgid): + effect = "partial" if self.aslr_ulimit else "no" + message = "Specfied aslr=False on setuid binary %s\n" % python.executable + message += "This will have %s effect. Add setuid=False to disable ASLR for debugging.\n" % effect + + if self.aslr_ulimit: + message += "Unlimited stack size should de-randomize shared libraries." + + self.warn_once(message) + + elif not aslr: + self.warn_once("ASLR is disabled for %r!" % python.executable) + return python def which(self, program): @@ -1499,6 +1573,15 @@ def upload_dir(self, local, remote=None): self.error("Could not untar %r on the remote end\n%s" % (remote_tar, message)) def upload(self, file_or_directory, remote=None): + """upload(file_or_directory, remote=None) + + Upload a file or directory to the remote host. + + Arguments: + file_or_directory(str): Path to the file or directory to download. + remote(str): Local path to store the data. + By default, uses the working directory. + """ if isinstance(file_or_directory, str): file_or_directory = os.path.expanduser(file_or_directory) file_or_directory = os.path.expandvars(file_or_directory) @@ -1512,18 +1595,40 @@ def upload(self, file_or_directory, remote=None): self.error('%r does not exist' % file_or_directory) - def download(self, file_or_directory, remote=None): + def download(self, file_or_directory, local=None): + """download(file_or_directory, local=None) + + Download a file or directory from the remote host. + + Arguments: + file_or_directory(str): Path to the file or directory to download. + local(str): Local path to store the data. + By default, uses the current directory. + """ if not self.sftp: self.error("Cannot determine remote file type without SFTP") if 0 == self.system(sh_string.sh_command_with('test -d %s', file_or_directory)).wait(): - self.download_dir(file_or_directory, remote) + self.download_dir(file_or_directory, local) else: - self.download_file(file_or_directory, remote) + self.download_file(file_or_directory, local) put = upload get = download + def unlink(self, file): + """unlink(file) + + Delete the file on the remote host + + Arguments: + file(str): Path to the file + """ + if not self.sftp: + self.error("unlink() is only supported if SFTP is supported") + + return self.sftp.unlink(file) + def libs(self, remote, directory = None): """Downloads the libraries referred to by a file. @@ -1773,17 +1878,18 @@ def distro(self): @property def aslr(self): """:class:`bool`: Whether ASLR is enabled on the system.""" - if self.os != 'linux': - self.warn_once("Only Linux is supported for ASLR checks.") - return False + if self._aslr is None: + if self.os != 'linux': + self.warn_once("Only Linux is supported for ASLR checks.") + self._aslr = False - with context.quiet: - rvs = self.read('/proc/sys/kernel/randomize_va_space') + else: + with context.quiet: + rvs = self.read('/proc/sys/kernel/randomize_va_space') - if rvs.startswith('0'): - return False + self._aslr = not rvs.startswith('0') - return True + return self._aslr @property def aslr_ulimit(self): @@ -1791,25 +1897,35 @@ def aslr_ulimit(self): import pwnlib.elf.elf import pwnlib.shellcraft + if self._aslr_ulimit is not None: + return self._aslr_ulimit + # This test must run a 32-bit binary, fix the architecture arch = { 'amd64': 'i386', 'aarch64': 'arm' }.get(self.arch, self.arch) - with context.local(arch=arch, bits=32, os=self.os, aslr=False): + with context.local(arch=arch, bits=32, os=self.os, aslr=True): with context.quiet: sc = pwnlib.shellcraft.cat('/proc/self/maps') \ + pwnlib.shellcraft.exit(0) elf = pwnlib.elf.elf.ELF.from_assembly(sc, shared=True) + def preexec(): + import resource + try: + resource.setrlimit(resource.RLIMIT_STACK, (-1, -1)) + except Exception: + pass + # Move to a new temporary directory cwd = self.cwd tmp = self.set_working_directory() self.upload(elf.path, './aslr-test') self.process(['chmod', '+x', './aslr-test']).wait() - maps = self.process(['./aslr-test']).recvall() + maps = self.process(['./aslr-test'], preexec_fn=preexec).recvall() # Move back to the old directory self.cwd = cwd @@ -1819,10 +1935,9 @@ def aslr_ulimit(self): # Check for 555555000 (1/3 of the address space for PAE) # and for 40000000 (1/3 of the address space with 3BG barrier) - if '55555000' in maps or '40000000' in maps: - return True + self._aslr_ulimit = bool('55555000' in maps or '40000000' in maps) - return False + return self._aslr_ulimit def checksec(self, banner=True): """checksec()