'Executes a process on the remote server, in the same fashion
as pwnlib.tubes.process.process.
To achieve this, a Python script is created to call ``os.execve``
with the appropriate arguments.
As an added bonus, the ``ssh_channel`` object returned has a
``pid`` property for the process pid.
Arguments:
argv(list):
List of arguments to pass into the process
executable(str):
Path to the executable to run.
If :const:`None`, ``argv[0]`` is used.
tty(bool):
Request a `tty` from the server.  This usually fixes buffering problems
by causing `libc` to write data immediately rather than buffering it.
However, this disables interpretation of control codes (e.g. Ctrl+C)
and breaks `.shutdown`.
cwd(str):
Working directory.  If :const:`None`, uses the working directory specified
on :attr:`cwd` or set via :meth:`set_working_directory`.
env(dict):
Environment variables to set in the child.  If :const:`None`, inherits the
default environment.
timeout(int):
Timeout to set on the `tube` created to interact with the process.
run(bool):
Set to :const:`True` to run the program (default).
If :const:`False`, returns the path to an executable Python script on the
remote server which, when executed, will do it.
stdin(int, str):
If an integer, replace stdin with the numbered file descriptor.
If a string, a open a file with the specified path and replace
stdin with its file descriptor.  May also be one of ``sys.stdin``,
``sys.stdout``, ``sys.stderr``.  If :const:`None`, the file descriptor is closed.
stdout(int, str):
See ``stdin``.
stderr(int, str):
See ``stdin``.
preexec_fn(callable):
Function which is executed on the remote side before execve().
This **MUST** be a self-contained function -- it must perform
all of its own imports, and cannot refer to variables outside
its scope.
preexec_args(object):
Argument passed to ``preexec_fn``.
This **MUST** only consist of native Python objects.
raw(bool):
If :const:`True`, disable TTY control code interpretation.
aslr(bool):
See :class:`pwnlib.tubes.process.process` for more information.
setuid(bool):
See :class:`pwnlib.tubes.process.process` for more information.
shell(bool):
Pass the command-line arguments to the shell.
Returns:
A new SSH channel, or a path to a script if ``run=False``.
Notes:
Requires Python on the remote server.
Examples:
>>> s = ssh(host=\'example.pwnme\',
...         user=\'travis\',
...         password=\'demopass\')
>>> sh = s.process(\'/bin/sh\', env={\'PS1\':\'\'})
>>> sh.sendline(\'echo Hello; exit\')
>>> sh.recvall()
\'Hello\n\'
>>> s.process([\'/bin/echo\', \'\xff\']).recvall()
\'\xff\n\'
>>> s.process([\'readlink\', \'/proc/self/exe\']).recvall()
\'/bin/readlink\n\'
>>> s.process([\'LOLOLOL\', \'/proc/self/exe\'], executable=\'readlink\').recvall()
\'/bin/readlink\n\'
>>> s.process([\'LOLOLOL\x00\', \'/proc/self/cmdline\'], executable=\'cat\').recvall()
\'LOLOLOL\x00/proc/self/cmdline\x00\'
>>> sh = s.process(executable=\'/bin/sh\')
>>> sh.pid in pidof(\'sh\') # doctest: +SKIP
True
>>> s.process([\'pwd\'], cwd=\'/tmp\').recvall()
\'/tmp\n\'
>>> p = s.process([\'python\',\'-c\',\'import os; print os.read(2, 1024)\'], stderr=0)
>>> p.send(\'hello\')
>>> p.recv()
\'hello\n\'
>>> s.process([\'/bin/echo\', \'hello\']).recvall()
\'hello\n\'
>>> s.process([\'/bin/echo\', \'hello\'], stdout=\'/dev/null\').recvall()
>>> s.process([\'/usr/bin/env\'], env={}).recvall()
>>> s.process(\'/usr/bin/env\', env={\'A\':\'B\'}).recvall()
\'A=B\n\'
>>> s.process(\'false\', preexec_fn=1234)
Traceback (most recent call last):
PwnlibException: preexec_fn must be a function
>>> s.process(\'false\', preexec_fn=lambda: 1234)
Traceback (most recent call last):
PwnlibException: preexec_fn cannot be a lambda
>>> def uses_globals():
...     foo = bar
>>> print s.process(\'false\', preexec_fn=uses_globals).recvall().strip() # doctest: +ELLIPSIS
Traceback (most recent call last):
NameError: global name \'bar\' is not defined
>>> s.process(\'echo hello\', shell=True).recvall()
\'hello\n\''

def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, timeout=Timeout.default, run=True, stdin=0, stdout=1, stderr=2, preexec_fn=None, preexec_args=[], raw=True, aslr=None, setuid=None, shell=False):

    if ((not argv) and (not executable)):
        self.error('Must    specify    argv    or    executable')
    argv = (argv or [])
    aslr = (aslr if (aslr is not None) else context.aslr)
    if isinstance(argv, (str, unicode)):
        argv = [argv]
    if (not isinstance(argv, (list, tuple))):
        self.error('argv    must    be    a    list    or    tuple')
    if shell:
        if (len(argv) != 1):
            self.error('Cannot    provide    more    than    1    argument    if    shell=True')
        argv = (['/bin/sh', '-c'] + argv)
    for (i, arg) in enumerate(argv):
        if ('\x00' in arg[:(-1)]):
            self.error(('Inappropriate    nulls    in    argv[%i]:    %r' % (i, arg)))
        argv[i] = arg.rstrip('\x00')
    if (env and hasattr(env, 'items')):
        for (k, v) in env.items():
            if ('\x00' in k[:(-1)]):
                self.error(('Inappropriate    nulls    in    environment    key    %r' % k))
            if ('\x00' in v[:(-1)]):
                self.error(('Inappropriate    nulls    in    environment    value    %r=%r' % (k, v)))
            env[k.rstrip('\x00')] = v.rstrip('\x00')
    executable = (executable or argv[0])
    cwd = (cwd or self.cwd)
    if (not isinstance(executable, str)):
        self.error(('executable    /    argv[0]    must    be    a    string:    %r' % executable))
    if (not isinstance(argv, (list, tuple))):
        self.error(('argv    must    be    a    list    or    tuple:    %r' % argv))
    if ((env is not None) and (not isinstance(env, dict)) and (env != os.environ)):
        self.error(('env    must    be    a    dict:    %r' % env))
    if (not all((isinstance(s, str) for s in argv))):
        self.error(('argv    must    only    contain    strings:    %r' % argv))
    handles = {sys.stdin: 0, sys.stdout: 1, sys.stderr: 2}
    stdin = handles.get(stdin, stdin)
    stdout = handles.get(stdout, stdout)
    stderr = handles.get(stderr, stderr)
    def func():
        pass
    func = (preexec_fn or func)
    func_args = preexec_args
    if (not isinstance(func, types.FunctionType)):
        self.error('preexec_fn    must    be    a    function')
    func_name = func.__name__
    if (func_name == (lambda : 0).__name__):
        self.error('preexec_fn    cannot    be    a    lambda')
    func_src = inspect.getsource(func).strip()
    setuid = (True if (setuid is None) else bool(setuid))
    script = ('\n#!/usr/bin/env    python2\nimport    os,    sys,    ctypes,    resource,    platform,    stat\nfrom    collections    import    OrderedDict\nexe            =    %(executable)r\nargv        =    %(argv)r\nenv            =    %(env)r\n\nos.chdir(%(cwd)r)\n\nif    env    is    not    None:\n                os.environ.clear()\n                os.environ.update(env)\n\ndef    is_exe(path):\n                return    os.path.isfile(path)    and    os.access(path,    os.X_OK)\n\nPATH    =    os.environ.get(\'PATH\',\'\').split(os.pathsep)\n\nif    os.path.sep    not    in    exe    and    not    is_exe(exe):\n                for    path    in    PATH:\n                                test_path    =    os.path.join(path,    exe)\n                                if    is_exe(test_path):\n                                                exe    =    test_path\n                                                break\n\nif    not    is_exe(exe):\n                sys.stderr.write(\'3\\n\')\n                sys.stderr.write("{}    is    not    executable    or    does    not    exist    in    $PATH:    {}".format(exe,PATH))\n                sys.exit(-1)\n\nif    not    %(setuid)r:\n                PR_SET_NO_NEW_PRIVS    =    38\n                result    =    ctypes.CDLL(\'libc.so.6\').prctl(PR_SET_NO_NEW_PRIVS,    1,    0,    0,    0)\n\n                if    result    !=    0:\n                                sys.stdout.write(\'3\\n\')\n                                sys.stdout.write("Could    not    disable    setuid:    prctl(PR_SET_NO_NEW_PRIVS)    failed")\n                                sys.exit(-1)\n\ntry:\n                PR_SET_PTRACER    =    0x59616d61\n                PR_SET_PTRACER_ANY    =    -1\n                ctypes.CDLL(\'libc.so.6\').prctl(PR_SET_PTRACER,    PR_SET_PTRACER_ANY,    0,    0,    0)\nexcept    Exception:\n                pass\n\n#    Determine    what    UID    the    process    will    execute    as\n#    This    is    used    for    locating    apport    core    dumps\nsuid    =    os.getuid()\nsgid    =    os.getgid()\nst    =    os.stat(exe)\nif    %(setuid)r:\n                if    (st.st_mode    &    stat.S_ISUID):\n                                suid    =    st.st_uid\n                if    (st.st_mode    &    stat.S_ISGID):\n                                sgid    =    st.st_gid\n\nif    sys.argv[-1]    ==    \'check\':\n                sys.stdout.write("1\\n")\n                sys.stdout.write(str(os.getpid())    +    "\\n")\n                sys.stdout.write(str(os.getuid())    +    "\\n")\n                sys.stdout.write(str(os.getgid())    +    "\\n")\n                sys.stdout.write(str(suid)    +    "\\n")\n                sys.stdout.write(str(sgid)    +    "\\n")\n                sys.stdout.write(os.path.realpath(exe)    +    \'\\x00\')\n                sys.stdout.flush()\n\nfor    fd,    newfd    in    {0:    %(stdin)r,    1:    %(stdout)r,    2:%(stderr)r}.items():\n                if    newfd    is    None:\n                                close(fd)\n                elif    isinstance(newfd,    str):\n                                os.close(fd)\n                                os.open(newfd,    os.O_RDONLY    if    fd    ==    0    else    (os.O_RDWR|os.O_CREAT))\n                elif    isinstance(newfd,    int)    and    newfd    !=    fd:\n                                os.dup2(fd,    newfd)\n\nif    not    %(aslr)r:\n                if    platform.system().lower()    ==    \'linux\'    and    %(setuid)r    is    not    True:\n                                ADDR_NO_RANDOMIZE    =    0x0040000\n                                ctypes.CDLL(\'libc.so.6\').personality(ADDR_NO_RANDOMIZE)\n\n                resource.setrlimit(resource.RLIMIT_STACK,    (-1,    -1))\n\n#    Attempt    to    dump    ALL    core    file    regions\ntry:\n                with    open(\'/proc/self/coredump_filter\',    \'w\')    as    core_filter:\n                                core_filter.write(\'0x3f\\n\')\nexcept    Exception:\n                pass\n\n#    Assume    that    the    user    would    prefer    to    have    core    dumps.\ntry:\n                resource.setrlimit(resource.RLIMIT_CORE,    (-1,    -1))\nexcept    Exception:\n                pass\n\n%(func_src)s\napply(%(func_name)s,    %(func_args)r)\n\nos.execve(exe,    argv,    os.environ)\n' % locals())
    script = script.strip()
    self.debug(('Created    execve    script:\n' + script))
    if (not run):
        with context.local(log_level='error'):
            tmpfile = self.mktemp('-t', 'pwnlib-execve-XXXXXXXXXX')
            self.chmod('+x', tmpfile)
        self.info(('Uploading    execve    script    to    %r' % tmpfile))
        self.upload_data(script, tmpfile)
        return tmpfile
    if self.isEnabledFor(logging.DEBUG):
        execve_repr = ('execve(%r,    %s,    %s)' % (executable, argv, ('os.environ' if (env in (None, os.environ)) else env)))
        if (self.isEnabledFor(logging.DEBUG) and (len(execve_repr) > 512)):
            execve_repr = (execve_repr[:512] + '...')
    else:
        execve_repr = repr(executable)
    msg = ('Starting    remote    process    %s    on    %s' % (execve_repr, self.host))
    with self.progress(msg) as h:
        script = ('for    py    in    python2.7    python2    python;    do    test    -x    "$(which    $py    2>&1)"    &&    exec    $py    -c    %s    check;    done;    echo    2' % sh_string(script))
        with context.local(log_level='error'):
            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 (result != 1):
            error_message = python.recvrepeat(timeout=1)
        if (result == 0):
            self.error(('%r    does    not    exist    or    is    not    executable' % executable))
        elif (result == 3):
            self.error(error_message)
        elif (result == 2):
            self.error(('python    is    not    installed    on    the    remote    system    %r' % self.host))
        elif (result != 1):
            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
