• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Gallopsled / pwntools / 7250412654

18 Dec 2023 03:44PM UTC coverage: 74.547% (+0.1%) from 74.452%
7250412654

push

github

web-flow
Merge branch 'dev' into retguard

4565 of 7244 branches covered (0.0%)

350 of 507 new or added lines in 17 files covered. (69.03%)

13 existing lines in 5 files now uncovered.

12843 of 17228 relevant lines covered (74.55%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.75
/pwnlib/tubes/ssh.py
1
from __future__ import absolute_import
1✔
2
from __future__ import division
1✔
3

4
import inspect
1✔
5
import logging
1✔
6
import os
1✔
7
import re
1✔
8
import shutil
1✔
9
import six
1✔
10
import string
1✔
11
import sys
1✔
12
import tarfile
1✔
13
import tempfile
1✔
14
import threading
1✔
15
import time
1✔
16
import types
1✔
17

18
from pwnlib import term
1✔
19
from pwnlib.context import context, LocalContext
1✔
20
from pwnlib.exception import PwnlibException
1✔
21
from pwnlib.log import Logger
1✔
22
from pwnlib.log import getLogger
1✔
23
from pwnlib.term import text
1✔
24
from pwnlib.timeout import Timeout
1✔
25
from pwnlib.tubes.sock import sock
1✔
26
from pwnlib.util import hashes
1✔
27
from pwnlib.util import misc
1✔
28
from pwnlib.util import packing
1✔
29
from pwnlib.util import safeeval
1✔
30
from pwnlib.util.sh_string import sh_string
1✔
31

32
# Kill the warning line:
33
# No handlers could be found for logger "paramiko.transport"
34
paramiko_log = logging.getLogger("paramiko.transport")
1✔
35
h = logging.StreamHandler(open(os.devnull,'w+'))
1✔
36
h.setFormatter(logging.Formatter())
1✔
37
paramiko_log.addHandler(h)
1✔
38

39
class ssh_channel(sock):
1✔
40

41
    #: Parent :class:`ssh` object
42
    parent = None
1✔
43

44
    #: Remote host
45
    host = None
1✔
46

47
    #: Return code, or :const:`None` if the process has not returned
48
    #: Use :meth:`poll` to check.
49
    returncode = None
1✔
50

51
    #: :const:`True` if a tty was allocated for this channel
52
    tty = False
1✔
53

54
    #: Environment specified for the remote process, or :const:`None`
55
    #: if the default environment was used
56
    env = None
1✔
57

58
    #: Command specified for the constructor
59
    process = None
1✔
60

61
    def __init__(self, parent, process = None, tty = False, cwd = None, env = None, raw = True, *args, **kwargs):
1✔
62
        super(ssh_channel, self).__init__(*args, **kwargs)
1✔
63

64
        # keep the parent from being garbage collected in some cases
65
        self.parent = parent
1✔
66

67
        self.returncode = None
1✔
68
        self.host = parent.host
1✔
69
        self.tty  = tty
1✔
70
        self.env  = env
1✔
71
        self.process = process
1✔
72
        self.cwd  = cwd or '.'
1✔
73
        if isinstance(cwd, six.text_type):
1✔
74
            cwd = packing._need_bytes(cwd, 2, 0x80)
1✔
75

76
        env = env or {}
1✔
77
        msg = 'Opening new channel: %r' % (process or 'shell')
1✔
78

79
        if isinstance(process, (list, tuple)):
1✔
80
            process = b' '.join(sh_string(packing._need_bytes(s, 2, 0x80)) for s in process)
1✔
81
        if isinstance(process, six.text_type):
1✔
82
            process = packing._need_bytes(process, 2, 0x80)
1✔
83

84
        if process and cwd:
1!
85
            process = b'cd ' + sh_string(cwd) + b' >/dev/null 2>&1; ' + process
1✔
86

87
        if process and env:
1✔
88
            for name, value in env.items():
1✔
89
                nameb = packing._need_bytes(name, 2, 0x80)
1✔
90
                if not re.match(b'^[a-zA-Z_][a-zA-Z0-9_]*$', nameb):
1!
91
                    self.error('run(): Invalid environment key %r' % name)
×
92
                export = b'export %s=%s;' % (nameb, sh_string(packing._need_bytes(value, 2, 0x80)))
1✔
93
                process = export + process
1✔
94

95
        if process and tty:
1✔
96
            if raw:
1!
97
                process = b'stty raw -ctlecho -echo; ' + process
1✔
98
            else:
99
                process = b'stty -ctlecho -echo; ' + process
×
100

101

102
        # If this object is enabled for DEBUG-level logging, don't hide
103
        # anything about the command that's actually executed.
104
        if process and self.isEnabledFor(logging.DEBUG):
1!
105
            msg = 'Opening new channel: %r' % ((process,) or 'shell')
×
106

107
        with self.waitfor(msg) as h:
1✔
108
            import paramiko
1✔
109
            try:
1✔
110
                self.sock = parent.transport.open_session()
1✔
111
            except paramiko.ChannelException as e:
×
112
                if e.args == (1, 'Administratively prohibited'):
×
113
                    self.error("Too many sessions open! Use ssh_channel.close() or 'with'!")
×
114
                raise e
×
115

116
            if self.tty:
1✔
117
                self.sock.get_pty('xterm', term.width, term.height)
1✔
118

119
                def resizer():
1✔
120
                    if self.sock:
×
121
                        try:
×
122
                            self.sock.resize_pty(term.width, term.height)
×
123
                        except paramiko.ssh_exception.SSHException:
×
124
                            pass
×
125

126
                self.resizer = resizer
1✔
127
                term.term.on_winch.append(self.resizer)  # XXX memory leak
1✔
128
            else:
129
                self.resizer = None
1✔
130

131
            # Put stderr on stdout. This might not always be desirable,
132
            # but our API does not support multiple streams
133
            self.sock.set_combine_stderr(True)
1✔
134

135
            self.settimeout(self.timeout)
1✔
136

137
            if process:
1!
138
                self.sock.exec_command(process)
1✔
139
            else:
140
                self.sock.invoke_shell()
×
141

142
            h.success()
1✔
143

144
    def kill(self):
1✔
145
        """kill()
146

147
        Kills the process.
148
        """
149

150
        self.close()
×
151

152
    def recvall(self, timeout = sock.forever):
1✔
153
        # We subclass tubes.sock which sets self.sock to None.
154
        #
155
        # However, we need to wait for the return value to propagate,
156
        # which may not happen by the time .close() is called by tube.recvall()
157
        tmp_sock = self.sock
1✔
158
        tmp_close = self.close
1✔
159
        self.close = lambda: None
1✔
160

161
        timeout = self.maximum if self.timeout is self.forever else self.timeout
1✔
162
        data = super(ssh_channel, self).recvall(timeout)
1✔
163

164
        # Restore self.sock to be able to call wait()
165
        self.close = tmp_close
1✔
166
        self.sock = tmp_sock
1✔
167
        self.wait()
1✔
168
        self.close()
1✔
169

170
        # Again set self.sock to None
171
        self.sock = None
1✔
172

173
        return data
1✔
174

175
    def wait(self, timeout=sock.default):
1✔
176
        # TODO: deal with timeouts
177
        return self.poll(block=True)
1✔
178

179
    def poll(self, block=False):
1✔
180
        """poll() -> int
181

182
        Poll the exit code of the process. Will return None, if the
183
        process has not yet finished and the exit code otherwise.
184
        """
185

186
        if self.returncode is None and self.sock \
1✔
187
        and (block or self.sock.exit_status_ready()):
188
            while not self.sock.status_event.is_set():
1✔
189
                self.sock.status_event.wait(0.05)
1✔
190
            self.returncode = self.sock.recv_exit_status()
1✔
191

192
        return self.returncode
1✔
193

194
    def can_recv_raw(self, timeout):
1✔
195
        with self.countdown(timeout):
×
196
            while self.countdown_active():
×
197
                if self.sock.recv_ready():
×
198
                    return True
×
199
                time.sleep(min(self.timeout, 0.05))
×
200
        return False
×
201

202
    def interactive(self, prompt = term.text.bold_red('$') + ' '):
1✔
203
        """interactive(prompt = pwnlib.term.text.bold_red('$') + ' ')
204

205
        If not in TTY-mode, this does exactly the same as
206
        meth:`pwnlib.tubes.tube.tube.interactive`, otherwise
207
        it does mostly the same.
208

209
        An SSH connection in TTY-mode will typically supply its own prompt,
210
        thus the prompt argument is ignored in this case.
211
        We also have a few SSH-specific hacks that will ideally be removed
212
        once the :mod:`pwnlib.term` is more mature.
213
        """
214

215
        # If we are only executing a regular old shell, we need to handle
216
        # control codes (specifically Ctrl+C).
217
        #
218
        # Otherwise, we can just punt to the default implementation of interactive()
219
        if self.process is not None:
×
220
            return super(ssh_channel, self).interactive(prompt)
×
221

222
        self.info('Switching to interactive mode')
×
223

224
        # We would like a cursor, please!
225
        term.term.show_cursor()
×
226

227
        event = threading.Event()
×
228
        def recv_thread(event):
×
229
            while not event.is_set():
×
230
                try:
×
231
                    cur = self.recv(timeout = 0.05)
×
232
                    cur = cur.replace(b'\r\n',b'\n')
×
233
                    cur = cur.replace(b'\r',b'')
×
234
                    if cur is None:
×
235
                        continue
×
236
                    elif cur == b'\a':
×
237
                        # Ugly hack until term unstands bell characters
238
                        continue
×
239
                    stdout = sys.stdout
×
240
                    if not term.term_mode:
×
241
                        stdout = getattr(stdout, 'buffer', stdout)
×
242
                    stdout.write(cur)
×
243
                    stdout.flush()
×
244
                except EOFError:
×
245
                    self.info('Got EOF while reading in interactive')
×
246
                    event.set()
×
247
                    break
×
248

249
        t = context.Thread(target = recv_thread, args = (event,))
×
250
        t.daemon = True
×
251
        t.start()
×
252

253
        while not event.is_set():
×
254
            if term.term_mode:
×
255
                try:
×
256
                    data = term.key.getraw(0.1)
×
257
                except KeyboardInterrupt:
×
258
                    data = [3] # This is ctrl-c
×
259
                except IOError:
×
260
                    if not event.is_set():
×
261
                        raise
×
262
            else:
263
                stdin = getattr(sys.stdin, 'buffer', sys.stdin)
×
264
                data = stdin.read(1)
×
265
                if not data:
×
266
                    event.set()
×
267
                else:
268
                    data = bytearray(data)
×
269

270
            if data:
×
271
                try:
×
272
                    self.send(bytes(bytearray(data)))
×
273
                except EOFError:
×
274
                    event.set()
×
275
                    self.info('Got EOF while sending in interactive')
×
276

277
        while t.is_alive():
×
278
            t.join(timeout = 0.1)
×
279

280
        # Restore
281
        term.term.hide_cursor()
×
282

283
    def close(self):
1✔
284
        self.poll()
1✔
285
        while self.resizer in term.term.on_winch:
1✔
286
            term.term.on_winch.remove(self.resizer)
1✔
287
        super(ssh_channel, self).close()
1✔
288

289
    def spawn_process(self, *args, **kwargs):
1✔
290
        self.error("Cannot use spawn_process on an SSH channel.""")
×
291

292
    def _close_msg(self):
1✔
293
        self.info('Closed SSH channel with %s' % self.host)
1✔
294

295
class ssh_process(ssh_channel):
1✔
296
    #: Working directory
297
    cwd = None
1✔
298

299
    #: PID of the process
300
    #: Only valid when instantiated through :meth:`ssh.process`
301
    pid = None
1✔
302

303
    #: Executable of the procesks
304
    #: Only valid when instantiated through :meth:`ssh.process`
305
    executable = None
1✔
306

307
    #: Arguments passed to the process
308
    #: Only valid when instantiated through :meth:`ssh.process`
309
    argv = None
1✔
310

311
    def libs(self):
1✔
312
        """libs() -> dict
313

314
        Returns a dictionary mapping the address of each loaded library in the
315
        process's address space.
316

317
        If ``/proc/$PID/maps`` cannot be opened, the output of ldd is used
318
        verbatim, which may be different than the actual addresses if ASLR
319
        is enabled.
320
        """
321
        maps = self.parent.libs(self.executable)
1✔
322

323
        maps_raw = self.parent.cat('/proc/%d/maps' % self.pid).decode()
1✔
324

325
        for lib in maps:
1✔
326
            remote_path = lib.split(self.parent.host)[-1]
1✔
327
            for line in maps_raw.splitlines():
1✔
328
                if line.endswith(remote_path):
1!
329
                    address = line.split('-')[0]
×
330
                    maps[lib] = int(address, 16)
×
331
                    break
×
332
        return maps
1✔
333

334

335
    @property
1✔
336
    def libc(self):
1✔
337
        """libc() -> ELF
338

339
        Returns an ELF for the libc for the current process.
340
        If possible, it is adjusted to the correct address
341
        automatically.
342

343
        Examples:
344
            >>> s =  ssh(host='example.pwnme')
345
            >>> p = s.process('true')
346
            >>> p.libc  # doctest: +ELLIPSIS
347
            ELF(.../libc.so.6')
348
        """
349
        from pwnlib.elf import ELF
1✔
350

351
        for lib, address in self.libs().items():
1!
352
            if 'libc.so' in lib:
1!
353
                e = ELF(lib)
1✔
354
                e.address = address
1✔
355
                return e
1✔
356

357
    @property
1✔
358
    def elf(self):
1✔
359
        """elf() -> pwnlib.elf.elf.ELF
360

361
        Returns an ELF file for the executable that launched the process.
362
        """
363
        import pwnlib.elf.elf
×
364

365
        libs = self.parent.libs(self.executable)
×
366

367
        for lib in libs:
×
368
            # Cannot just check "executable in lib", see issue #1047
369
            if lib.endswith(self.executable):
×
370
                return pwnlib.elf.elf.ELF(lib)
×
371

372

373
    @property
1✔
374
    def corefile(self):
1✔
375
        import pwnlib.elf.corefile
×
376

377
        finder = pwnlib.elf.corefile.CorefileFinder(self)
×
378
        if not finder.core_path:
×
379
            self.error("Could not find core file for pid %i" % self.pid)
×
380

381
        return pwnlib.elf.corefile.Corefile(finder.core_path)
×
382

383
    def getenv(self, variable, **kwargs):
1✔
384
        r"""Retrieve the address of an environment variable in the remote process.
385

386
        Examples:
387
            >>> s = ssh(host='example.pwnme')
388
            >>> p = s.process(['python', '-c', 'import time; time.sleep(10)'])
389
            >>> hex(p.getenv('PATH'))  # doctest: +ELLIPSIS
390
            '0x...'
391
        """
392
        argv0 = self.argv[0]
1✔
393

394
        variable = bytearray(packing._need_bytes(variable, min_wrong=0x80))
1✔
395

396
        script = ';'.join(('from ctypes import *',
1✔
397
                           'import os',
398
                           'libc = CDLL("libc.so.6")',
399
                           'getenv = libc.getenv',
400
                           'getenv.restype = c_void_p',
401
                           'print(os.path.realpath(%r))' % self.executable,
402
                           'print(getenv(bytes(%r)))' % variable,))
403

404
        try:
1✔
405
            with context.quiet:
1✔
406
                python = self.parent.which('python2.7') or self.parent.which('python3') or self.parent.which('python')
1✔
407

408
                if not python:
1!
409
                    self.error("Python is not installed on the remote system.")
×
410

411
                io = self.parent.process([argv0,'-c', script.strip()],
1✔
412
                                          executable=python,
413
                                          env=self.env,
414
                                          **kwargs)
415
                path = io.recvline()
1✔
416
                address = int(io.recvall())
1✔
417

418
                address -= len(python)
1✔
419
                address += len(path)
1✔
420

421
                return int(address) & context.mask
1✔
422
        except Exception:
×
423
            self.exception("Could not look up environment variable %r" % variable)
×
424

425
    def _close_msg(self):
1✔
426
        # If we never completely started up, just use the parent implementation
427
        if self.executable is None:
1!
428
            return super(ssh_process, self)._close_msg()
×
429

430
        self.info('Stopped remote process %r on %s (pid %i)' \
1✔
431
            % (os.path.basename(self.executable),
432
               self.host,
433
               self.pid))
434

435

436
class ssh_connecter(sock):
1✔
437
    def __init__(self, parent, host, port, *a, **kw):
1✔
438
        super(ssh_connecter, self).__init__(*a, **kw)
1✔
439

440
        # keep the parent from being garbage collected in some cases
441
        self.parent = parent
1✔
442

443
        self.host  = parent.host
1✔
444
        self.rhost = host
1✔
445
        self.rport = port
1✔
446

447
        msg = 'Connecting to %s:%d via SSH to %s' % (self.rhost, self.rport, self.host)
1✔
448
        with self.waitfor(msg) as h:
1✔
449
            try:
1✔
450
                self.sock = parent.transport.open_channel('direct-tcpip', (host, port), ('127.0.0.1', 0))
1✔
451
            except Exception as e:
×
452
                self.exception(e.message)
×
453
                raise
×
454

455
            try:
1✔
456
                # Iterate all layers of proxying to get to base-level Socket object
457
                curr = self.sock.get_transport().sock
1✔
458
                while getattr(curr, "get_transport", None):
1!
459
                    curr = curr.get_transport().sock
×
460

461
                sockname = curr.getsockname()
1✔
462
                self.lhost = sockname[0]
1✔
463
                self.lport = sockname[1]
1✔
464
            except Exception as e:
×
465
                self.exception("Could not find base-level Socket object.")
×
466
                raise e
×
467

468
            h.success()
1✔
469

470
    def spawn_process(self, *args, **kwargs):
1✔
471
        self.error("Cannot use spawn_process on an SSH channel.""")
×
472

473
    def _close_msg(self):
1✔
474
        self.info("Closed remote connection to %s:%d via SSH connection to %s" % (self.rhost, self.rport, self.host))
1✔
475

476

477
class ssh_listener(sock):
1✔
478
    def __init__(self, parent, bind_address, port, *a, **kw):
1✔
479
        super(ssh_listener, self).__init__(*a, **kw)
1✔
480

481
        # keep the parent from being garbage collected in some cases
482
        self.parent = parent
1✔
483

484
        self.host = parent.host
1✔
485

486
        try:
1✔
487
            self.port = parent.transport.request_port_forward(bind_address, port)
1✔
488

489
        except Exception:
×
490
            h.failure('Failed create a port forwarding')
×
491
            raise
×
492

493
        def accepter():
1✔
494
            msg = 'Waiting on port %d via SSH to %s' % (self.port, self.host)
1✔
495
            h   = self.waitfor(msg)
1✔
496
            try:
1✔
497
                self.sock = parent.transport.accept()
1✔
498
                parent.transport.cancel_port_forward(bind_address, self.port)
1✔
499
            except Exception:
×
500
                self.sock = None
×
501
                h.failure()
×
502
                self.exception('Failed to get a connection')
×
503
                return
×
504

505
            self.rhost, self.rport = self.sock.origin_addr
1✔
506
            h.success('Got connection from %s:%d' % (self.rhost, self.rport))
1✔
507

508
        self._accepter = context.Thread(target = accepter)
1✔
509
        self._accepter.daemon = True
1✔
510
        self._accepter.start()
1✔
511

512
    def _close_msg(self):
1✔
513
        self.info("Closed remote connection to %s:%d via SSH listener on port %d via %s" % (self.rhost, self.rport, self.port, self.host))
×
514

515
    def spawn_process(self, *args, **kwargs):
1✔
516
        self.error("Cannot use spawn_process on an SSH channel.""")
×
517

518
    def wait_for_connection(self):
1✔
519
        """Blocks until a connection has been established."""
520
        _ = self.sock
1✔
521
        return self
1✔
522

523
    @property
1✔
524
    def sock(self):
1✔
525
        try:
1✔
526
            return self.__dict__['sock']
1✔
527
        except KeyError:
1✔
528
            pass
1✔
529
        while self._accepter.is_alive():
1✔
530
            self._accepter.join(timeout=0.1)
1✔
531
        return self.__dict__.get('sock')
1✔
532

533
    @sock.setter
1✔
534
    def sock(self, s):
1✔
535
        self.__dict__['sock'] = s
1✔
536

537

538
class ssh(Timeout, Logger):
1✔
539

540
    #: Remote host name (``str``)
541
    host = None
1✔
542

543
    #: Remote port (``int``)
544
    port = None
1✔
545

546
    #: Enable caching of SSH downloads (``bool``)
547
    cache = True
1✔
548

549
    #: Paramiko SSHClient which backs this object
550
    client = None
1✔
551

552
    #: Paramiko SFTPClient object which is used for file transfers.
553
    #: Set to :const:`None` to disable ``sftp``.
554
    sftp = None
1✔
555

556
    #: PID of the remote ``sshd`` process servicing this connection.
557
    pid = None
1✔
558

559
    _cwd = '.'
1✔
560

561
    def __init__(self, user=None, host=None, port=22, password=None, key=None,
1✔
562
                 keyfile=None, proxy_command=None, proxy_sock=None, level=None,
563
                 cache=True, ssh_agent=False, ignore_config=False, raw=False, *a, **kw):
564
        """Creates a new ssh connection.
565

566
        Arguments:
567
            user(str): The username to log in with
568
            host(str): The hostname to connect to
569
            port(int): The port to connect to
570
            password(str): Try to authenticate using this password
571
            key(str): Try to authenticate using this private key. The string should be the actual private key.
572
            keyfile(str): Try to authenticate using this private key. The string should be a filename.
573
            proxy_command(str): Use this as a proxy command. It has approximately the same semantics as ProxyCommand from ssh(1).
574
            proxy_sock(str): Use this socket instead of connecting to the host.
575
            timeout: Timeout, in seconds
576
            level: Log level
577
            cache: Cache downloaded files (by hash/size/timestamp)
578
            ssh_agent: If :const:`True`, enable usage of keys via ssh-agent
579
            ignore_config: If :const:`True`, disable usage of ~/.ssh/config and ~/.ssh/authorized_keys
580
            raw: If :const:`True`, assume a non-standard shell and don't probe the environment
581

582
        NOTE: The proxy_command and proxy_sock arguments is only available if a
583
        fairly new version of paramiko is used.
584

585
        Example proxying:
586

587
        .. doctest::
588
           :skipif: True
589

590
            >>> s1 = ssh(host='example.pwnme')
591
            >>> r1 = s1.remote('localhost', 22)
592
            >>> s2 = ssh(host='example.pwnme', proxy_sock=r1.sock)
593
            >>> r2 = s2.remote('localhost', 22) # and so on...
594
            >>> for x in r2, s2, r1, s1: x.close()
595
        """
596
        super(ssh, self).__init__(*a, **kw)
1✔
597

598
        Logger.__init__(self)
1✔
599
        if level is not None:
1!
600
            self.setLevel(level)
×
601

602

603
        self.host            = host
1✔
604
        self.port            = port
1✔
605
        self.user            = user
1✔
606
        self.password        = password
1✔
607
        self.key             = key
1✔
608
        self.keyfile         = keyfile
1✔
609
        self._cachedir       = os.path.join(tempfile.gettempdir(), 'pwntools-ssh-cache')
1✔
610
        self.cache           = cache
1✔
611
        self.raw             = raw
1✔
612

613
        # Deferred attributes
614
        self._platform_info = {}
1✔
615
        self._aslr = None
1✔
616
        self._aslr_ulimit = None
1✔
617
        self._cpuinfo_cache = None
1✔
618
        self._user_shstk = None
1✔
619
        self._ibt = None
1✔
620

621
        misc.mkdir_p(self._cachedir)
1✔
622

623
        import paramiko
1✔
624

625
        # Make a basic attempt to parse the ssh_config file
626
        try:
1✔
627
            config_file = os.path.expanduser('~/.ssh/config')
1✔
628

629
            if not ignore_config and os.path.exists(config_file):
1!
630
                ssh_config  = paramiko.SSHConfig()
1✔
631
                ssh_config.parse(open(config_file))
1✔
632
                host_config = ssh_config.lookup(host)
1✔
633
                if 'hostname' in host_config:
1!
634
                    self.host = host = host_config['hostname']
1✔
635
                if not user and 'user' in host_config:
1✔
636
                    self.user = user = host_config['user']
1✔
637
                if not keyfile and 'identityfile' in host_config:
1!
638
                    keyfile = host_config['identityfile'][0]
1✔
639
                    if keyfile.lower() == 'none':
1!
640
                        keyfile = None
×
641
        except Exception as e:
×
642
            self.debug("An error occurred while parsing ~/.ssh/config:\n%s" % e)
×
643

644
        keyfiles = [os.path.expanduser(keyfile)] if keyfile else []
1✔
645

646
        msg = 'Connecting to %s on port %d' % (host, port)
1✔
647
        with self.waitfor(msg) as h:
1✔
648
            self.client = paramiko.SSHClient()
1✔
649
            self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1✔
650

651
            if not ignore_config:
1!
652
                known_hosts = os.path.expanduser('~/.ssh/known_hosts')
1✔
653
                if os.path.exists(known_hosts):
1!
654
                    self.client.load_host_keys(known_hosts)
1✔
655

656
            has_proxy = bool(proxy_sock or proxy_command)
1✔
657
            if has_proxy:
1!
658
                if 'ProxyCommand' not in dir(paramiko):
×
659
                    self.error('This version of paramiko does not support proxies.')
×
660

661
                if proxy_sock and proxy_command:
×
662
                    self.error('Cannot have both a proxy command and a proxy sock')
×
663

664
                if proxy_command:
×
665
                    proxy_sock = paramiko.ProxyCommand(proxy_command)
×
666
            else:
667
                proxy_sock = None
1✔
668

669
            try:
1✔
670
                self.client.connect(host, port, user, password, key, keyfiles, self.timeout, allow_agent=ssh_agent, compress=True, sock=proxy_sock, look_for_keys=not ignore_config)
1✔
671
            except paramiko.BadHostKeyException as e:
×
672
                self.error("Remote host %(host)s is using a different key than stated in known_hosts\n"
×
673
                           "    To remove the existing entry from your known_hosts and trust the new key, run the following commands:\n"
674
                           "        $ ssh-keygen -R %(host)s\n"
675
                           "        $ ssh-keygen -R [%(host)s]:%(port)s" % locals())
676

677
            self.transport = self.client.get_transport()
1✔
678
            self.transport.use_compression(True)
1✔
679

680
            h.success()
1✔
681

682
        if self.raw:
1!
683
            return
×
684

685
        self._tried_sftp = False
1✔
686

687
        if self.sftp:
1!
688
            with context.quiet:
1✔
689
                self.cwd = packing._decode(self.pwd())
1✔
690
        else:
691
            self.cwd = '.'
×
692

693
        with context.local(log_level='error'):
1✔
694
            def getppid():
1✔
695
                print(os.getppid())
×
696
            try:
1✔
697
                self.pid = int(self.process('false', preexec_fn=getppid).recvall())
1✔
698
            except Exception:
×
699
                self.pid = None
×
700

701
        try:
1✔
702
            self.info_once(self.checksec())
1✔
703
        except Exception:
×
704
            self.warn_once("Couldn't check security settings on %r" % self.host)
×
705

706
    def __repr__(self):
1✔
707
        return "{}(user={!r}, host={!r})".format(self.__class__.__name__, self.user, self.host)
1✔
708

709
    @property
1✔
710
    def cwd(self):
1✔
711
        return self._cwd
1✔
712

713
    @cwd.setter
1✔
714
    def cwd(self, cwd):
1✔
715
        self._cwd = cwd
1✔
716
        if self.sftp:
1!
717
            self.sftp.chdir(cwd)
1✔
718

719
    @property
1✔
720
    def sftp(self):
1✔
721
        if not self._tried_sftp:
1✔
722
            try:
1✔
723
                self._sftp = self.transport.open_sftp_client()
1✔
724
            except Exception:
×
725
                self._sftp = None
×
726

727
        self._tried_sftp = True
1✔
728
        return self._sftp
1✔
729

730
    @sftp.setter
1✔
731
    def sftp(self, value):
1✔
732
        self._sftp = value
×
733
        self._tried_sftp = True
×
734

735
    def __enter__(self, *a):
1✔
736
        return self
×
737

738
    def __exit__(self, *a, **kw):
1✔
739
        self.close()
×
740

741
    def shell(self, shell = None, tty = True, timeout = Timeout.default):
1✔
742
        """shell(shell = None, tty = True, timeout = Timeout.default) -> ssh_channel
743

744
        Open a new channel with a shell inside.
745

746
        Arguments:
747
            shell(str): Path to the shell program to run.
748
                If :const:`None`, uses the default shell for the logged in user.
749
            tty(bool): If :const:`True`, then a TTY is requested on the remote server.
750

751
        Returns:
752
            Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
753

754
        Examples:
755
            >>> s =  ssh(host='example.pwnme')
756
            >>> sh = s.shell('/bin/sh')
757
            >>> sh.sendline(b'echo Hello; exit')
758
            >>> print(b'Hello' in sh.recvall())
759
            True
760
        """
761
        return self.run(shell, tty, timeout = timeout)
1✔
762

763
    def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, ignore_environ=None, timeout=Timeout.default, run=True,
1✔
764
                stdin=0, stdout=1, stderr=2, preexec_fn=None, preexec_args=(), raw=True, aslr=None, setuid=None,
765
                shell=False):
766
        r"""
767
        Executes a process on the remote server, in the same fashion
768
        as pwnlib.tubes.process.process.
769

770
        To achieve this, a Python script is created to call ``os.execve``
771
        with the appropriate arguments.
772

773
        As an added bonus, the ``ssh_channel`` object returned has a
774
        ``pid`` property for the process pid.
775

776
        Arguments:
777
            argv(list):
778
                List of arguments to pass into the process
779
            executable(str):
780
                Path to the executable to run.
781
                If :const:`None`, ``argv[0]`` is used.
782
            tty(bool):
783
                Request a `tty` from the server.  This usually fixes buffering problems
784
                by causing `libc` to write data immediately rather than buffering it.
785
                However, this disables interpretation of control codes (e.g. Ctrl+C)
786
                and breaks `.shutdown`.
787
            cwd(str):
788
                Working directory.  If :const:`None`, uses the working directory specified
789
                on :attr:`cwd` or set via :meth:`set_working_directory`.
790
            env(dict):
791
                Environment variables to add to the environment.
792
            ignore_environ(bool):
793
                Ignore default environment.  By default use default environment iff env not specified.
794
            timeout(int):
795
                Timeout to set on the `tube` created to interact with the process.
796
            run(bool):
797
                Set to :const:`True` to run the program (default).
798
                If :const:`False`, returns the path to an executable Python script on the
799
                remote server which, when executed, will do it.
800
            stdin(int, str):
801
                If an integer, replace stdin with the numbered file descriptor.
802
                If a string, a open a file with the specified path and replace
803
                stdin with its file descriptor.  May also be one of ``sys.stdin``,
804
                ``sys.stdout``, ``sys.stderr``.  If :const:`None`, the file descriptor is closed.
805
            stdout(int, str):
806
                See ``stdin``.
807
            stderr(int, str):
808
                See ``stdin``.
809
            preexec_fn(callable):
810
                Function which is executed on the remote side before execve().
811
                This **MUST** be a self-contained function -- it must perform
812
                all of its own imports, and cannot refer to variables outside
813
                its scope.
814
            preexec_args(object):
815
                Argument passed to ``preexec_fn``.
816
                This **MUST** only consist of native Python objects.
817
            raw(bool):
818
                If :const:`True`, disable TTY control code interpretation.
819
            aslr(bool):
820
                See :class:`pwnlib.tubes.process.process` for more information.
821
            setuid(bool):
822
                See :class:`pwnlib.tubes.process.process` for more information.
823
            shell(bool):
824
                Pass the command-line arguments to the shell.
825

826
        Returns:
827
            A new SSH channel, or a path to a script if ``run=False``.
828

829
        Notes:
830
            Requires Python on the remote server.
831

832
        Examples:
833
            >>> s = ssh(host='example.pwnme')
834
            >>> sh = s.process('/bin/sh', env={'PS1':''})
835
            >>> sh.sendline(b'echo Hello; exit')
836
            >>> sh.recvall()
837
            b'Hello\n'
838
            >>> s.process(['/bin/echo', b'\xff']).recvall()
839
            b'\xff\n'
840
            >>> s.process(['readlink', '/proc/self/exe']).recvall() # doctest: +ELLIPSIS
841
            b'.../bin/readlink\n'
842
            >>> s.process(['LOLOLOL', '/proc/self/exe'], executable='readlink').recvall() # doctest: +ELLIPSIS
843
            b'.../bin/readlink\n'
844
            >>> s.process(['LOLOLOL\x00', '/proc/self/cmdline'], executable='cat').recvall()
845
            b'LOLOLOL\x00/proc/self/cmdline\x00'
846
            >>> sh = s.process(executable='/bin/sh')
847
            >>> str(sh.pid).encode() in s.pidof('sh') # doctest: +SKIP
848
            True
849
            >>> io = s.process(['pwd'], cwd='/tmp')
850
            >>> io.recvall()
851
            b'/tmp\n'
852
            >>> io.cwd
853
            '/tmp'
854
            >>> p = s.process(['python','-c','import os; os.write(1, os.read(2, 1024))'], stderr=0)
855
            >>> p.send(b'hello')
856
            >>> p.recv()
857
            b'hello'
858
            >>> s.process(['/bin/echo', 'hello']).recvall()
859
            b'hello\n'
860
            >>> s.process(['/bin/echo', 'hello'], stdout='/dev/null').recvall()
861
            b''
862
            >>> s.process(['/usr/bin/env'], env={}).recvall()
863
            b''
864
            >>> s.process('/usr/bin/env', env={'A':'B'}).recvall()
865
            b'A=B\n'
866

867
            >>> s.process('false', preexec_fn=1234)
868
            Traceback (most recent call last):
869
            ...
870
            PwnlibException: preexec_fn must be a function
871

872
            >>> s.process('false', preexec_fn=lambda: 1234)
873
            Traceback (most recent call last):
874
            ...
875
            PwnlibException: preexec_fn cannot be a lambda
876

877
            >>> def uses_globals():
878
            ...     foo = bar
879
            >>> print(s.process('false', preexec_fn=uses_globals).recvall().strip().decode()) # doctest: +ELLIPSIS
880
            Traceback (most recent call last):
881
            ...
882
            NameError: ...name 'bar' is not defined
883

884
            >>> s.process('echo hello', shell=True).recvall()
885
            b'hello\n'
886

887
            >>> io = s.process(['cat'], timeout=5)
888
            >>> io.recvline()
889
            b''
890

891
            >>> # Testing that empty argv works
892
            >>> io = s.process([], executable='sh')
893
            >>> io.sendline(b'echo $0')
894
            >>> io.recvline()
895
            b'$ \n'
896
            >>> # Make sure that we have a shell
897
            >>> io.sendline(b'echo hello')
898
            >>> io.recvline()
899
            b'$ hello\n'
900

901
            >>> # Testing that empty argv[0] works
902
            >>> io = s.process([''], executable='sh')
903
            >>> io.sendline(b'echo $0')
904
            >>> io.recvline()
905
            b'$ \n'
906

907
        """
908
        if not argv and not executable:
1!
909
            self.error("Must specify argv or executable")
×
910

911
        aslr      = aslr if aslr is not None else context.aslr
1✔
912

913
        if ignore_environ is None:
1!
914
            ignore_environ = env is not None  # compat
1✔
915

916
        argv, env = misc.normalize_argv_env(argv, env, self)
1✔
917

918
        if shell:
1✔
919
            if len(argv) != 1:
1!
920
                self.error('Cannot provide more than 1 argument if shell=True')
×
921
            argv = [bytearray(b'/bin/sh'), bytearray(b'-c')] + argv
1✔
922

923
        executable = executable or argv[0]
1✔
924
        cwd        = cwd or self.cwd
1✔
925

926
        # Validate, since failures on the remote side will suck.
927
        if not isinstance(executable, (six.text_type, six.binary_type, bytearray)):
1!
928
            self.error("executable / argv[0] must be a string: %r" % executable)
×
929
        executable = bytearray(packing._need_bytes(executable, min_wrong=0x80))
1✔
930

931
        # Allow passing in sys.stdin/stdout/stderr objects
932
        handles = {sys.stdin: 0, sys.stdout:1, sys.stderr:2}
1✔
933
        stdin  = handles.get(stdin, stdin)
1✔
934
        stdout = handles.get(stdout, stdout)
1✔
935
        stderr = handles.get(stderr, stderr)
1✔
936

937
        # Allow the user to provide a self-contained function to run
938
        def func(): pass
1!
939
        func      = preexec_fn or func
1✔
940
        func_args = preexec_args
1✔
941

942
        if not isinstance(func, types.FunctionType):
1✔
943
            self.error("preexec_fn must be a function")
1✔
944

945
        func_name = func.__name__
1✔
946
        if func_name == (lambda: 0).__name__:
1!
947
            self.error("preexec_fn cannot be a lambda")
1✔
948

949
        func_src  = inspect.getsource(func).strip()
1✔
950
        setuid = True if setuid is None else bool(setuid)
1✔
951

952
        script = r"""
1✔
953
#!/usr/bin/env python
954
import os, sys, ctypes, resource, platform, stat
955
from collections import OrderedDict
956
try:
957
    integer_types = int, long
958
except NameError:
959
    integer_types = int,
960
exe   = bytes(%(executable)r)
961
argv  = [bytes(a) for a in %(argv)r]
962
env   = %(env)r
963

964
os.chdir(%(cwd)r)
965

966
if %(ignore_environ)r:
967
    os.environ.clear()
968
environ = getattr(os, 'environb', os.environ)
969

970
if env is not None:
971
    env = OrderedDict((bytes(k), bytes(v)) for k,v in env)
972
    environ.update(env)
973
else:
974
    env = environ
975

976
def is_exe(path):
977
    return os.path.isfile(path) and os.access(path, os.X_OK)
978

979
PATH = environ.get(b'PATH',b'').split(os.pathsep.encode())
980

981
if os.path.sep.encode() not in exe and not is_exe(exe):
982
    for path in PATH:
983
        test_path = os.path.join(path, exe)
984
        if is_exe(test_path):
985
            exe = test_path
986
            break
987

988
if not is_exe(exe):
989
    sys.stderr.write('3\n')
990
    sys.stderr.write("{!r} is not executable or does not exist in $PATH: {!r}".format(exe,PATH))
991
    sys.exit(-1)
992

993
if not %(setuid)r:
994
    PR_SET_NO_NEW_PRIVS = 38
995
    result = ctypes.CDLL('libc.so.6').prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
996

997
    if result != 0:
998
        sys.stdout.write('3\n')
999
        sys.stdout.write("Could not disable setuid: prctl(PR_SET_NO_NEW_PRIVS) failed")
1000
        sys.exit(-1)
1001

1002
try:
1003
    PR_SET_PTRACER = 0x59616d61
1004
    PR_SET_PTRACER_ANY = -1
1005
    ctypes.CDLL('libc.so.6').prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0)
1006
except Exception:
1007
    pass
1008

1009
# Determine what UID the process will execute as
1010
# This is used for locating apport core dumps
1011
suid = os.getuid()
1012
sgid = os.getgid()
1013
st = os.stat(exe)
1014
if %(setuid)r:
1015
    if (st.st_mode & stat.S_ISUID):
1016
        suid = st.st_uid
1017
    if (st.st_mode & stat.S_ISGID):
1018
        sgid = st.st_gid
1019

1020
if sys.argv[-1] == 'check':
1021
    sys.stdout.write("1\n")
1022
    sys.stdout.write(str(os.getpid()) + "\n")
1023
    sys.stdout.write(str(os.getuid()) + "\n")
1024
    sys.stdout.write(str(os.getgid()) + "\n")
1025
    sys.stdout.write(str(suid) + "\n")
1026
    sys.stdout.write(str(sgid) + "\n")
1027
    getattr(sys.stdout, 'buffer', sys.stdout).write(os.path.realpath(exe) + b'\x00')
1028
    sys.stdout.flush()
1029

1030
for fd, newfd in {0: %(stdin)r, 1: %(stdout)r, 2:%(stderr)r}.items():
1031
    if newfd is None:
1032
        os.close(fd)
1033
    elif isinstance(newfd, (str, bytes)):
1034
        newfd = os.open(newfd, os.O_RDONLY if fd == 0 else (os.O_RDWR|os.O_CREAT))
1035
        os.dup2(newfd, fd)
1036
        os.close(newfd)
1037
    elif isinstance(newfd, integer_types) and newfd != fd:
1038
        os.dup2(fd, newfd)
1039

1040
if not %(aslr)r:
1041
    if platform.system().lower() == 'linux' and %(setuid)r is not True:
1042
        ADDR_NO_RANDOMIZE = 0x0040000
1043
        ctypes.CDLL('libc.so.6').personality(ADDR_NO_RANDOMIZE)
1044

1045
    resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
1046

1047
# Attempt to dump ALL core file regions
1048
try:
1049
    with open('/proc/self/coredump_filter', 'w') as core_filter:
1050
        core_filter.write('0x3f\n')
1051
except Exception:
1052
    pass
1053

1054
# Assume that the user would prefer to have core dumps.
1055
try:
1056
    resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
1057
except Exception:
1058
    pass
1059

1060
%(func_src)s
1061
%(func_name)s(*%(func_args)r)
1062

1063
""" % locals()  
1064

1065
        if len(argv) > 0 and len(argv[0]) > 0:
1✔
1066
            script += r"os.execve(exe, argv, env) " 
1✔
1067

1068
        # os.execve does not allow us to pass empty argv[0]
1069
        # Therefore we use ctypes to call execve directly
1070
        else:
1071
            script += r"""
1✔
1072
# Transform envp from dict to list
1073
env_list = [key + b"=" + value for key, value in env.items()]
1074

1075
# ctypes helper to convert a python list to a NULL-terminated C array
1076
def to_carray(py_list):
1077
    py_list += [None] # NULL-terminated
1078
    return (ctypes.c_char_p * len(py_list))(*py_list)
1079

1080
c_argv = to_carray(argv)
1081
c_env = to_carray(env_list)
1082

1083
# Call execve
1084
libc = ctypes.CDLL('libc.so.6')
1085
libc.execve(exe, c_argv, c_env)
1086

1087
# We should never get here, since we sanitized argv and env,
1088
# but just in case, indicate that something went wrong.
1089
libc.perror(b"execve")
1090
raise OSError("execve failed")
1091
""" % locals()
1092

1093
        script = script.strip()
1✔
1094

1095
        self.debug("Created execve script:\n" + script)
1✔
1096

1097
        if not run:
1!
1098
            with context.local(log_level='error'):
×
1099
                tmpfile = self.mktemp('-t', 'pwnlib-execve-XXXXXXXXXX')
×
1100
                self.chmod('+x', tmpfile)
×
1101

1102
            self.info("Uploading execve script to %r" % tmpfile)
×
1103
            self.upload_data(script, tmpfile)
×
1104
            return tmpfile
×
1105

1106
        if self.isEnabledFor(logging.DEBUG):
1!
1107
            execve_repr = "execve(%r, %s, %s)" % (executable,
×
1108
                                                  argv,
1109
                                                  'os.environ'
1110
                                                  if (env in (None, getattr(os, "environb", os.environ))) 
1111
                                                  else env)
1112
            # Avoid spamming the screen
1113
            if self.isEnabledFor(logging.DEBUG) and len(execve_repr) > 512:
×
1114
                execve_repr = execve_repr[:512] + '...'
×
1115
        else:
1116
            execve_repr = repr(executable)
1✔
1117

1118
        msg = 'Starting remote process %s on %s' % (execve_repr, self.host)
1✔
1119

1120
        if timeout == Timeout.default:
1✔
1121
            timeout = self.timeout
1✔
1122

1123
        with self.progress(msg) as h:
1!
1124

1125
            script = 'echo PWNTOOLS; for py in python3 python2.7 python2 python; do test -x "$(command -v $py 2>&1)" && echo $py && exec $py -c %s check; done; echo 2' % sh_string(script)
1✔
1126
            with context.quiet:
1✔
1127
                python = ssh_process(self, script, tty=True, cwd=cwd, raw=True, level=self.level, timeout=timeout)
1✔
1128

1129
            try:
1✔
1130
                python.recvline_contains(b'PWNTOOLS')        # Magic flag so that any sh/bash initialization errors are swallowed
1✔
1131
                python.recvline()                           # Python interpreter that was selected
1✔
1132
                result = safeeval.const(python.recvline())  # Status flag from the Python script
1✔
1133
            except (EOFError, ValueError):
×
1134
                h.failure("Process creation failed")
×
1135
                self.warn_once('Could not find a Python interpreter on %s\n' % self.host
×
1136
                               + "Use ssh.run() instead of ssh.process()\n"
1137
                                 "The original error message:\n"
1138
                               + python.recvall().decode())
1139
                return None
×
1140

1141
            # If an error occurred, try to grab as much output
1142
            # as we can.
1143
            if result != 1:
1!
1144
                error_message = python.recvrepeat(timeout=1)
×
1145

1146
            if result == 0:
1!
1147
                self.error("%r does not exist or is not executable" % executable)
×
1148
            elif result == 3:
1!
1149
                self.error("%r" % error_message)
×
1150
            elif result == 2:
1!
1151
                self.error("python is not installed on the remote system %r" % self.host)
×
1152
            elif result != 1:
1!
1153
                h.failure("something bad happened:\n%s" % error_message)
×
1154

1155
            python.pid  = safeeval.const(python.recvline())
1✔
1156
            python.uid  = safeeval.const(python.recvline())
1✔
1157
            python.gid  = safeeval.const(python.recvline())
1✔
1158
            python.suid = safeeval.const(python.recvline())
1✔
1159
            python.sgid = safeeval.const(python.recvline())
1✔
1160
            python.argv = argv
1✔
1161
            python.executable = packing._decode(python.recvuntil(b'\x00')[:-1])
1✔
1162

1163
            h.success('pid %i' % python.pid)
1✔
1164

1165
        if not aslr and setuid and (python.uid != python.suid or python.gid != python.sgid):
1!
1166
            effect = "partial" if self.aslr_ulimit else "no"
×
1167
            message = "Specfied aslr=False on setuid binary %s\n" % python.executable
×
1168
            message += "This will have %s effect.  Add setuid=False to disable ASLR for debugging.\n" % effect
×
1169

1170
            if self.aslr_ulimit:
×
1171
                message += "Unlimited stack size should de-randomize shared libraries."
×
1172

1173
            self.warn_once(message)
×
1174

1175
        elif not aslr:
1!
1176
            self.warn_once("ASLR is disabled for %r!" % python.executable)
×
1177

1178
        return python
1✔
1179

1180
    def which(self, program):
1✔
1181
        """which(program) -> str
1182

1183
        Minor modification to just directly invoking ``which`` on the remote
1184
        system which adds the current working directory to the end of ``$PATH``.
1185
        """
1186
        # If name is a path, do not attempt to resolve it.
1187
        if os.path.sep in program:
1✔
1188
            return program
1✔
1189

1190
        result = self.run('export PATH=$PATH:$PWD; command -v %s' % program).recvall().strip().decode()
1✔
1191

1192
        if ('/%s' % program) not in result:
1✔
1193
            return None
1✔
1194

1195
        return result
1✔
1196

1197
    def system(self, process, tty = True, cwd = None, env = None, timeout = None, raw = True, wd = None):
1✔
1198
        r"""system(process, tty = True, cwd = None, env = None, timeout = Timeout.default, raw = True) -> ssh_channel
1199

1200
        Open a new channel with a specific process inside. If `tty` is True,
1201
        then a TTY is requested on the remote server.
1202

1203
        If `raw` is True, terminal control codes are ignored and input is not
1204
        echoed back.
1205

1206
        Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
1207

1208
        Examples:
1209
            >>> s =  ssh(host='example.pwnme')
1210
            >>> py = s.system('python3 -i')
1211
            >>> _ = py.recvuntil(b'>>> ')
1212
            >>> py.sendline(b'print(2+2)')
1213
            >>> py.sendline(b'exit()')
1214
            >>> print(repr(py.recvline()))
1215
            b'4\n'
1216
            >>> s.system('env | grep -a AAAA', env={'AAAA': b'\x90'}).recvall()
1217
            b'AAAA=\x90\n'
1218
            >>> io = s.system('pwd', cwd='/tmp')
1219
            >>> io.recvall()
1220
            b'/tmp\n'
1221
            >>> io.cwd
1222
            '/tmp'
1223
        """
1224
        if wd is not None:
1!
1225
            self.warning_once("The 'wd' argument to ssh.system() is deprecated.  Use 'cwd' instead.")
×
1226
            if cwd is None:
×
1227
                cwd = wd
×
1228
        if cwd is None:
1✔
1229
            cwd = self.cwd
1✔
1230

1231
        if timeout is None:
1✔
1232
            timeout = self.timeout
1✔
1233

1234
        return ssh_channel(self, process, tty, cwd, env, timeout = timeout, level = self.level, raw = raw)
1✔
1235

1236
    #: Backward compatibility.  Use :meth:`system`
1237
    run = system
1✔
1238

1239
    def getenv(self, variable, **kwargs):
1✔
1240
        """Retrieve the address of an environment variable on the remote
1241
        system.
1242

1243
        Note:
1244

1245
            The exact address will differ based on what other environment
1246
            variables are set, as well as argv[0].  In order to ensure that
1247
            the path is *exactly* the same, it is recommended to invoke the
1248
            process with ``argv=[]``.
1249
        """
1250
        script = '''
×
1251
from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r))
1252
''' % variable
1253

1254
        with context.local(log_level='error'):
×
1255
            python = self.which('python') or self.which('python2.7') or self.which('python3')
×
1256

1257
            if not python:
×
1258
                self.error("Python is not installed on the remote system.")
×
1259

1260
            io = self.process(['','-c', script.strip()], executable=python, **kwargs)
×
1261
            result = io.recvall()
×
1262

1263
        try:
×
1264
            return int(result) & context.mask
×
1265
        except ValueError:
×
1266
            self.exception("Could not look up environment variable %r" % variable)
×
1267

1268

1269

1270
    def run_to_end(self, process, tty = False, cwd = None, env = None, wd = None):
1✔
1271
        r"""run_to_end(process, tty = False, cwd = None, env = None, timeout = Timeout.default) -> str
1272

1273
        Run a command on the remote server and return a tuple with
1274
        (data, exit_status). If `tty` is True, then the command is run inside
1275
        a TTY on the remote server.
1276

1277
        Examples:
1278
            >>> s =  ssh(host='example.pwnme')
1279
            >>> print(s.run_to_end('echo Hello; exit 17'))
1280
            (b'Hello\n', 17)
1281
            """
1282

1283
        if wd is not None:
1✔
1284
            self.warning_once("The 'wd' argument to ssh.run_to_end() is deprecated.  Use 'cwd' instead.")
1✔
1285
            if cwd is None:
1!
1286
                cwd = wd
1✔
1287

1288
        with context.local(log_level = 'ERROR'):
1✔
1289
            c = self.run(process, tty, cwd = cwd, env = env, timeout = Timeout.default)
1✔
1290
            data = c.recvall()
1✔
1291
            retcode = c.wait()
1✔
1292
            c.close()
1✔
1293
            return data, retcode
1✔
1294

1295
    def connect_remote(self, host, port, timeout = Timeout.default):
1✔
1296
        r"""connect_remote(host, port, timeout = Timeout.default) -> ssh_connecter
1297

1298
        Connects to a host through an SSH connection. This is equivalent to
1299
        using the ``-L`` flag on ``ssh``.
1300

1301
        Returns a :class:`pwnlib.tubes.ssh.ssh_connecter` object.
1302

1303
        Examples:
1304
            >>> from pwn import *
1305
            >>> l = listen()
1306
            >>> s =  ssh(host='example.pwnme')
1307
            >>> a = s.connect_remote(s.host, l.lport)
1308
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1309
            >>> a.sendline(b'Hello')
1310
            >>> print(repr(b.recvline()))
1311
            b'Hello\n'
1312
        """
1313

1314
        return ssh_connecter(self, host, port, timeout, level=self.level)
1✔
1315

1316
    remote = connect_remote
1✔
1317

1318
    def listen_remote(self, port = 0, bind_address = '', timeout = Timeout.default):
1✔
1319
        r"""listen_remote(port = 0, bind_address = '', timeout = Timeout.default) -> ssh_connecter
1320

1321
        Listens remotely through an SSH connection. This is equivalent to
1322
        using the ``-R`` flag on ``ssh``.
1323

1324
        Returns a :class:`pwnlib.tubes.ssh.ssh_listener` object.
1325

1326
        Examples:
1327

1328
            >>> from pwn import *
1329
            >>> s =  ssh(host='example.pwnme')
1330
            >>> l = s.listen_remote()
1331
            >>> a = remote(s.host, l.port)
1332
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1333
            >>> a.sendline(b'Hello')
1334
            >>> print(repr(b.recvline()))
1335
            b'Hello\n'
1336
        """
1337

1338
        return ssh_listener(self, bind_address, port, timeout, level=self.level)
1✔
1339

1340
    listen = listen_remote
1✔
1341

1342
    def __getitem__(self, attr):
1✔
1343
        """Permits indexed access to run commands over SSH
1344

1345
        Examples:
1346

1347
            >>> s =  ssh(host='example.pwnme')
1348
            >>> print(repr(s['echo hello']))
1349
            b'hello'
1350
        """
1351
        return self.run(attr).recvall().strip()
1✔
1352

1353
    def __call__(self, attr):
1✔
1354
        """Permits function-style access to run commands over SSH
1355

1356
        Examples:
1357

1358
            >>> s =  ssh(host='example.pwnme')
1359
            >>> print(repr(s('echo hello')))
1360
            b'hello'
1361
        """
1362
        return self.run(attr).recvall().strip()
1✔
1363

1364
    def __getattr__(self, attr):
1✔
1365
        """Permits member access to run commands over SSH
1366

1367
        Examples:
1368

1369
            >>> s =  ssh(host='example.pwnme')
1370
            >>> s.echo('hello')
1371
            b'hello'
1372
            >>> s.whoami()
1373
            b'travis'
1374
            >>> s.echo(['huh','yay','args'])
1375
            b'huh yay args'
1376
        """
1377
        bad_attrs = [
1✔
1378
            'trait_names',          # ipython tab-complete
1379
        ]
1380

1381
        if attr in self.__dict__ \
1!
1382
        or attr in bad_attrs \
1383
        or attr.startswith('_'):
1384
            raise AttributeError
×
1385

1386
        @LocalContext
1✔
1387
        def runner(*args):
1✔
1388
            if len(args) == 1 and isinstance(args[0], (list, tuple)):
1✔
1389
                command = [attr]
1✔
1390
                command.extend(args[0])
1✔
1391
            else:
1392
                command = [attr]
1✔
1393
                command.extend(args)
1✔
1394
                command = b' '.join(packing._need_bytes(arg, min_wrong=0x80) for arg in command)
1✔
1395

1396
            return self.run(command).recvall().strip()
1✔
1397
        return runner
1✔
1398

1399
    def connected(self):
1✔
1400
        """Returns True if we are connected.
1401

1402
        Example:
1403

1404
            >>> s =  ssh(host='example.pwnme')
1405
            >>> s.connected()
1406
            True
1407
            >>> s.close()
1408
            >>> s.connected()
1409
            False
1410
        """
1411
        return bool(self.client and self.client.get_transport().is_active())
1✔
1412

1413
    def close(self):
1✔
1414
        """Close the connection."""
1415
        if self.client:
1!
1416
            self.client.close()
1✔
1417
            self.client = None
1✔
1418
            self.info("Closed connection to %r" % self.host)
1✔
1419

1420
    def _libs_remote(self, remote):
1✔
1421
        """Return a dictionary of the libraries used by a remote file."""
1422
        escaped_remote = sh_string(remote)
1✔
1423
        cmd = ''.join([
1✔
1424
            '(',
1425
            'ulimit -s unlimited;',
1426
            'ldd %s > /dev/null &&' % escaped_remote,
1427
            '(',
1428
            'LD_TRACE_LOADED_OBJECTS=1 %s||' % escaped_remote,
1429
            'ldd %s' % escaped_remote,
1430
            '))',
1431
            ' 2>/dev/null'
1432
        ])
1433
        data, status = self.run_to_end(cmd)
1✔
1434
        if status != 0:
1!
1435
            self.error('Unable to find libraries for %r' % remote)
×
1436
            return {}
×
1437

1438
        return misc.parse_ldd_output(packing._decode(data))
1✔
1439

1440
    def _get_fingerprint(self, remote):
1✔
1441
        cmd = '(sha256 || sha256sum || openssl sha256) 2>/dev/null < '
1✔
1442
        cmd = cmd + sh_string(remote)
1✔
1443

1444
        data, status = self.run_to_end(cmd)
1✔
1445

1446
        if status != 0:
1!
1447
            return None
×
1448

1449
        if not isinstance(data, str):
1✔
1450
            data = data.decode('ascii')
1✔
1451

1452
        data = re.search("([a-fA-F0-9]{64})",data).group()
1✔
1453
        return data
1✔
1454

1455
    def _get_cachefile(self, fingerprint):
1✔
1456
        return os.path.join(self._cachedir, fingerprint)
1✔
1457

1458
    def _verify_local_fingerprint(self, fingerprint):
1✔
1459
        if not set(fingerprint).issubset(string.hexdigits) or \
1!
1460
           len(fingerprint) != 64:
1461
            self.error('Invalid fingerprint %r' % fingerprint)
×
1462
            return False
×
1463

1464
        local = self._get_cachefile(fingerprint)
1✔
1465
        if not os.path.isfile(local):
1✔
1466
            return False
1✔
1467

1468
        if hashes.sha256filehex(local) == fingerprint:
1!
1469
            return True
1✔
1470
        else:
1471
            os.unlink(local)
×
1472
            return False
×
1473

1474
    def _download_raw(self, remote, local, h):
1✔
1475
        def update(has, total):
1✔
1476
            h.status("%s/%s" % (misc.size(has), misc.size(total)))
1✔
1477

1478
        if self.sftp:
1✔
1479
            try:
1✔
1480
                self.sftp.get(remote, local, update)
1✔
1481
                return
1✔
1482
            except IOError:
×
1483
                pass
×
1484

1485
        cmd = 'wc -c < ' + sh_string(remote)
1✔
1486
        total, exitcode = self.run_to_end(cmd)
1✔
1487

1488
        if exitcode != 0:
1!
1489
            h.failure("%r does not exist or is not accessible" % remote)
×
1490
            return
×
1491

1492
        total = int(total)
1✔
1493

1494
        with context.local(log_level = 'ERROR'):
1✔
1495
            cmd = 'cat < ' + sh_string(remote)
1✔
1496
            c = self.run(cmd)
1✔
1497
        data = b''
1✔
1498

1499
        while True:
1✔
1500
            try:
1✔
1501
                data += c.recv()
1✔
1502
            except EOFError:
1✔
1503
                break
1✔
1504
            update(len(data), total)
1✔
1505

1506
        result = c.wait()
1✔
1507
        if result != 0:
1!
1508
            h.failure('Could not download file %r (%r)' % (remote, result))
×
1509
            return
×
1510

1511
        with open(local, 'wb') as fd:
1✔
1512
            fd.write(data)
1✔
1513

1514
    def _download_to_cache(self, remote, p, fingerprint=True):
1✔
1515

1516
        with context.local(log_level='error'):
1✔
1517
            remote = self.readlink('-f',remote)
1✔
1518
        if not hasattr(remote, 'encode'):
1✔
1519
            remote = remote.decode('utf-8')
1✔
1520

1521
        fingerprint = fingerprint and self._get_fingerprint(remote) or None
1✔
1522
        if fingerprint is None:
1✔
1523
            local = os.path.normpath(remote)
1✔
1524
            local = os.path.basename(local)
1✔
1525
            local += time.strftime('-%Y-%m-%d-%H:%M:%S')
1✔
1526
            local = os.path.join(self._cachedir, local)
1✔
1527

1528
            self._download_raw(remote, local, p)
1✔
1529
            return local
1✔
1530

1531
        local = self._get_cachefile(fingerprint)
1✔
1532

1533
        if self.cache and self._verify_local_fingerprint(fingerprint):
1✔
1534
            p.success('Found %r in ssh cache' % remote)
1✔
1535
        else:
1536
            self._download_raw(remote, local, p)
1✔
1537

1538
            if not self._verify_local_fingerprint(fingerprint):
1!
1539
                self.error('Could not download file %r', remote)
×
1540

1541
        return local
1✔
1542

1543
    def download_data(self, remote, fingerprint=True):
1✔
1544
        """Downloads a file from the remote server and returns it as a string.
1545

1546
        Arguments:
1547
            remote(str): The remote filename to download.
1548

1549

1550
        Examples:
1551
            >>> with open('/tmp/bar','w+') as f:
1552
            ...     _ = f.write('Hello, world')
1553
            >>> s =  ssh(host='example.pwnme',
1554
            ...         cache=False)
1555
            >>> s.download_data('/tmp/bar')
1556
            b'Hello, world'
1557
            >>> s._sftp = None
1558
            >>> s._tried_sftp = True
1559
            >>> s.download_data('/tmp/bar')
1560
            b'Hello, world'
1561

1562
        """
1563
        with self.progress('Downloading %r' % remote) as p:
1✔
1564
            with open(self._download_to_cache(remote, p, fingerprint), 'rb') as fd:
1✔
1565
                return fd.read()
1✔
1566

1567
    def download_file(self, remote, local = None):
1✔
1568
        """Downloads a file from the remote server.
1569

1570
        The file is cached in /tmp/pwntools-ssh-cache using a hash of the file, so
1571
        calling the function twice has little overhead.
1572

1573
        Arguments:
1574
            remote(str/bytes): The remote filename to download
1575
            local(str): The local filename to save it to. Default is to infer it from the remote filename.
1576
        
1577
        Examples:
1578
            >>> with open('/tmp/foobar','w+') as f:
1579
            ...     _ = f.write('Hello, world')
1580
            >>> s =  ssh(host='example.pwnme',
1581
            ...         cache=False)
1582
            >>> _ = s.set_working_directory(wd='/tmp')
1583
            >>> _ = s.download_file('foobar', 'barfoo')
1584
            >>> with open('barfoo','r') as f:
1585
            ...     print(f.read())
1586
            Hello, world
1587
        """
1588

1589

1590
        if not local:
1✔
1591
            local = os.path.basename(os.path.normpath(remote))
1✔
1592

1593
        with self.progress('Downloading %r to %r' % (remote, local)) as p:
1✔
1594
            local_tmp = self._download_to_cache(remote, p)
1✔
1595

1596
        # Check to see if an identical copy of the file already exists
1597
        if not os.path.exists(local) or hashes.sha256filehex(local_tmp) != hashes.sha256filehex(local):
1✔
1598
            shutil.copy2(local_tmp, local)
1✔
1599

1600
    def download_dir(self, remote=None, local=None, ignore_failed_read=False):
1✔
1601
        """Recursively downloads a directory from the remote server
1602

1603
        Arguments:
1604
            local: Local directory
1605
            remote: Remote directory
1606
        """
1607
        remote = packing._encode(remote or self.cwd)
×
1608

1609
        if self.sftp:
×
1610
            remote = packing._encode(self.sftp.normalize(remote))
×
1611
        else:
1612
            with context.local(log_level='error'):
×
1613
                remote = self.system(b'readlink -f ' + sh_string(remote)).recvall().strip()
×
1614

1615
        local = local or '.'
×
1616
        local = os.path.expanduser(local)
×
1617

1618
        self.info("Downloading %r to %r" % (remote, local))
×
1619

1620
        if ignore_failed_read:
×
1621
            opts = b" --ignore-failed-read"
×
1622
        else:
1623
            opts = b""
×
1624
        with context.local(log_level='error'):
×
1625
            remote_tar = self.mktemp()
×
1626
            cmd = b'tar %s -C %s -czf %s .' % \
×
1627
                  (opts,
1628
                   sh_string(remote),
1629
                   sh_string(remote_tar))
1630
            tar = self.system(cmd)
×
1631

1632
            if 0 != tar.wait():
×
1633
                self.error("Could not create remote tar")
×
1634

1635
            local_tar = tempfile.NamedTemporaryFile(suffix='.tar.gz')
×
1636
            self.download_file(remote_tar, local_tar.name)
×
1637

1638
            # Delete temporary tarfile from remote host
1639
            if self.sftp:
×
1640
                self.unlink(remote_tar)
×
1641
            else:
1642
                self.system(b'rm ' + sh_string(remote_tar)).wait()
×
1643
            tar = tarfile.open(local_tar.name)
×
1644
            tar.extractall(local)
×
1645

1646

1647
    def upload_data(self, data, remote):
1✔
1648
        """Uploads some data into a file on the remote server.
1649

1650
        Arguments:
1651
            data(str): The data to upload.
1652
            remote(str): The filename to upload it to.
1653

1654
        Example:
1655
            >>> s =  ssh(host='example.pwnme')
1656
            >>> s.upload_data(b'Hello, world', '/tmp/upload_foo')
1657
            >>> print(open('/tmp/upload_foo').read())
1658
            Hello, world
1659
            >>> s._sftp = False
1660
            >>> s._tried_sftp = True
1661
            >>> s.upload_data(b'Hello, world', '/tmp/upload_bar')
1662
            >>> print(open('/tmp/upload_bar').read())
1663
            Hello, world
1664
        """
1665
        data = packing._need_bytes(data)
1✔
1666
        # If a relative path was provided, prepend the cwd
1667
        if os.path.normpath(remote) == os.path.basename(remote):
1✔
1668
            remote = os.path.join(self.cwd, remote)
1✔
1669

1670
        if self.sftp:
1✔
1671
            with tempfile.NamedTemporaryFile() as f:
1✔
1672
                f.write(data)
1✔
1673
                f.flush()
1✔
1674
                self.sftp.put(f.name, remote)
1✔
1675
                return
1✔
1676

1677
        with context.local(log_level = 'ERROR'):
1✔
1678
            cmd = 'cat > ' + sh_string(remote)
1✔
1679
            s = self.run(cmd, tty=False)
1✔
1680
            s.send(data)
1✔
1681
            s.shutdown('send')
1✔
1682
            data   = s.recvall()
1✔
1683
            result = s.wait()
1✔
1684
            if result != 0:
1!
1685
                self.error("Could not upload file %r (%r)\n%s" % (remote, result, data))
×
1686

1687
    def upload_file(self, filename, remote = None):
1✔
1688
        """Uploads a file to the remote server. Returns the remote filename.
1689

1690
        Arguments:
1691
        filename(str): The local filename to download
1692
        remote(str): The remote filename to save it to. Default is to infer it from the local filename."""
1693

1694

1695
        if remote is None:
1✔
1696
            remote = os.path.normpath(filename)
1✔
1697
            remote = os.path.basename(remote)
1✔
1698
            remote = os.path.join(self.cwd, remote)
1✔
1699

1700
        with open(filename, 'rb') as fd:
1✔
1701
            data = fd.read()
1✔
1702

1703
        self.info("Uploading %r to %r" % (filename,remote))
1✔
1704
        self.upload_data(data, remote)
1✔
1705

1706
        return remote
1✔
1707

1708
    def upload_dir(self, local, remote=None):
1✔
1709
        """Recursively uploads a directory onto the remote server
1710

1711
        Arguments:
1712
            local: Local directory
1713
            remote: Remote directory
1714
        """
1715

1716
        remote    = packing._encode(remote or self.cwd)
×
1717

1718
        local     = os.path.expanduser(local)
×
1719
        dirname   = os.path.dirname(local)
×
1720
        basename  = os.path.basename(local)
×
1721

1722
        if not os.path.isdir(local):
×
1723
            self.error("%r is not a directory" % local)
×
1724

1725
        msg = "Uploading %r to %r" % (basename,remote)
×
1726
        with self.waitfor(msg):
×
1727
            # Generate a tarfile with everything inside of it
1728
            local_tar  = tempfile.mktemp()
×
1729
            with tarfile.open(local_tar, 'w:gz') as tar:
×
1730
                tar.add(local, basename)
×
1731

1732
            # Upload and extract it
1733
            with context.local(log_level='error'):
×
1734
                remote_tar = self.mktemp('--suffix=.tar.gz')
×
1735
                self.upload_file(local_tar, remote_tar)
×
1736

1737
                untar = self.run(b'cd %s && tar -xzf %s' % (sh_string(remote), sh_string(remote_tar)))
×
1738
                message = untar.recvrepeat(2)
×
1739

1740
                if untar.wait() != 0:
×
1741
                    self.error("Could not untar %r on the remote end\n%s" % (remote_tar, message))
×
1742

1743
    def upload(self, file_or_directory, remote=None):
1✔
1744
        """upload(file_or_directory, remote=None)
1745

1746
        Upload a file or directory to the remote host.
1747

1748
        Arguments:
1749
            file_or_directory(str): Path to the file or directory to download.
1750
            remote(str): Local path to store the data.
1751
                By default, uses the working directory.
1752
        """
1753
        if isinstance(file_or_directory, str):
1!
1754
            file_or_directory = os.path.expanduser(file_or_directory)
1✔
1755
            file_or_directory = os.path.expandvars(file_or_directory)
1✔
1756

1757
        if os.path.isfile(file_or_directory):
1!
1758
            return self.upload_file(file_or_directory, remote)
1✔
1759

1760
        if os.path.isdir(file_or_directory):
×
1761
            return self.upload_dir(file_or_directory, remote)
×
1762

1763
        self.error('%r does not exist' % file_or_directory)
×
1764

1765
    def download(self, file_or_directory, local=None):
1✔
1766
        """download(file_or_directory, local=None)
1767

1768
        Download a file or directory from the remote host.
1769

1770
        Arguments:
1771
            file_or_directory(str): Path to the file or directory to download.
1772
            local(str): Local path to store the data.
1773
                By default, uses the current directory.
1774
        
1775

1776
        Examples:
1777
            >>> with open('/tmp/foobar','w+') as f:
1778
            ...     _ = f.write('Hello, world')
1779
            >>> s =  ssh(host='example.pwnme',
1780
            ...         cache=False)
1781
            >>> _ = s.set_working_directory('/tmp')
1782
            >>> _ = s.download('foobar', 'barfoo')
1783
            >>> with open('barfoo','r') as f:
1784
            ...     print(f.read())
1785
            Hello, world
1786
        """
1787
        file_or_directory = packing._encode(file_or_directory)
1✔
1788
        with self.system(b'test -d ' + sh_string(file_or_directory)) as io:
1✔
1789
            is_dir = io.wait()
1✔
1790

1791
        if 0 == is_dir:
1!
1792
            self.download_dir(file_or_directory, local)
×
1793
        else:
1794
            self.download_file(file_or_directory, local)
1✔
1795

1796
    put = upload
1✔
1797
    get = download
1✔
1798

1799
    def unlink(self, file):
1✔
1800
        """unlink(file)
1801

1802
        Delete the file on the remote host
1803

1804
        Arguments:
1805
            file(str): Path to the file
1806
        """
1807
        if not self.sftp:
×
1808
            self.error("unlink() is only supported if SFTP is supported")
×
1809

1810
        return self.sftp.unlink(file)
×
1811

1812
    def libs(self, remote, directory = None):
1✔
1813
        """Downloads the libraries referred to by a file.
1814

1815
        This is done by running ldd on the remote server, parsing the output
1816
        and downloading the relevant files.
1817

1818
        The directory argument specified where to download the files. This defaults
1819
        to './$HOSTNAME' where $HOSTNAME is the hostname of the remote server."""
1820

1821
        libs = self._libs_remote(remote)
1✔
1822

1823
        remote = packing._decode(self.readlink('-f',remote).strip())
1✔
1824
        libs[remote] = 0
1✔
1825

1826
        if directory is None:
1!
1827
            directory = self.host
1✔
1828

1829
        directory = os.path.realpath(directory)
1✔
1830

1831
        res = {}
1✔
1832

1833
        seen = set()
1✔
1834

1835
        for lib, addr in libs.items():
1✔
1836
            local = os.path.realpath(os.path.join(directory, '.' + os.path.sep + lib))
1✔
1837
            if not local.startswith(directory):
1!
1838
                self.warning('This seems fishy: %r' % lib)
×
1839
                continue
×
1840

1841
            misc.mkdir_p(os.path.dirname(local))
1✔
1842

1843
            if lib not in seen:
1!
1844
                self.download_file(lib, local)
1✔
1845
                seen.add(lib)
1✔
1846
            res[local] = addr
1✔
1847

1848
        return res
1✔
1849

1850
    def interactive(self, shell=None):
1✔
1851
        """Create an interactive session.
1852

1853
        This is a simple wrapper for creating a new
1854
        :class:`pwnlib.tubes.ssh.ssh_channel` object and calling
1855
        :meth:`pwnlib.tubes.ssh.ssh_channel.interactive` on it."""
1856

1857
        s = self.shell(shell)
×
1858

1859
        if self.cwd != '.':
×
1860
            cmd = 'cd ' + sh_string(self.cwd)
×
1861
            s.sendline(packing._need_bytes(cmd, 2, 0x80))
×
1862

1863
        s.interactive()
×
1864
        s.close()
×
1865

1866
    def set_working_directory(self, wd = None, symlink = False):
1✔
1867
        """Sets the working directory in which future commands will
1868
        be run (via ssh.run) and to which files will be uploaded/downloaded
1869
        from if no path is provided
1870

1871
        Note:
1872
            This uses ``mktemp -d`` under the covers, sets permissions
1873
            on the directory to ``0700``.  This means that setuid binaries
1874
            will **not** be able to access files created in this directory.
1875

1876
            In order to work around this, we also ``chmod +x`` the directory.
1877

1878
        Arguments:
1879
            wd(string): Working directory.  Default is to auto-generate a directory
1880
                based on the result of running 'mktemp -d' on the remote machine.
1881
            symlink(bool,str): Create symlinks in the new directory.
1882

1883
                The default value, ``False``, implies that no symlinks should be
1884
                created.
1885

1886
                A string value is treated as a path that should be symlinked.
1887
                It is passed directly to the shell on the remote end for expansion,
1888
                so wildcards work.
1889

1890
                Any other value is treated as a boolean, where ``True`` indicates
1891
                that all files in the "old" working directory should be symlinked.
1892

1893
        Examples:
1894
            >>> s =  ssh(host='example.pwnme')
1895
            >>> cwd = s.set_working_directory()
1896
            >>> s.ls()
1897
            b''
1898
            >>> packing._decode(s.pwd()) == cwd
1899
            True
1900

1901
            >>> s =  ssh(host='example.pwnme')
1902
            >>> homedir = s.pwd()
1903
            >>> _=s.touch('foo')
1904

1905
            >>> _=s.set_working_directory()
1906
            >>> assert s.ls() == b''
1907

1908
            >>> _=s.set_working_directory(homedir)
1909
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1910

1911
            >>> _=s.set_working_directory(symlink=True)
1912
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1913
            >>> assert homedir != s.pwd()
1914

1915
            >>> symlink=os.path.join(homedir,b'*')
1916
            >>> _=s.set_working_directory(symlink=symlink)
1917
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1918
            >>> assert homedir != s.pwd()
1919

1920
            >>> _=s.set_working_directory()
1921
            >>> io = s.system('pwd')
1922
            >>> io.recvallS().strip() == io.cwd
1923
            True
1924
            >>> io.cwd == s.cwd
1925
            True
1926
        """
1927
        status = 0
1✔
1928

1929
        if symlink and not isinstance(symlink, (six.binary_type, six.text_type)):
1✔
1930
            symlink = os.path.join(self.pwd(), b'*')
1✔
1931
        if not hasattr(symlink, 'encode') and hasattr(symlink, 'decode'):
1✔
1932
            symlink = symlink.decode('utf-8')
1✔
1933
            
1934
        if isinstance(wd, six.text_type):
1✔
1935
            wd = packing._need_bytes(wd, 2, 0x80)
1✔
1936

1937
        if not wd:
1✔
1938
            wd, status = self.run_to_end('x=$(mktemp -d) && cd $x && chmod +x . && echo $PWD', cwd='.')
1✔
1939
            wd = wd.strip()
1✔
1940

1941
            if status:
1!
1942
                self.error("Could not generate a temporary directory (%i)\n%s" % (status, wd))
×
1943

1944
        else:
1945
            cmd = b'ls ' + sh_string(wd)
1✔
1946
            _, status = self.run_to_end(cmd, wd = '.')
1✔
1947

1948
            if status:
1!
1949
                self.error("%r does not appear to exist" % wd)
×
1950

1951
        if not isinstance(wd, str):
1✔
1952
            wd = wd.decode('utf-8')
1✔
1953
        self.cwd = wd
1✔
1954

1955
        self.info("Working directory: %r" % self.cwd)
1✔
1956

1957
        if symlink:
1✔
1958
            self.ln('-s', symlink, '.')
1✔
1959

1960
        return wd
1✔
1961

1962
    def write(self, path, data):
1✔
1963
        """Wrapper around upload_data to match :func:`pwnlib.util.misc.write`"""
1964
        data = packing._need_bytes(data)
1✔
1965
        return self.upload_data(data, path)
1✔
1966

1967
    def read(self, path):
1✔
1968
        """Wrapper around download_data to match :func:`pwnlib.util.misc.read`"""
1969
        return self.download_data(path)
1✔
1970

1971
    def _init_remote_platform_info(self):
1✔
1972
        r"""Fills _platform_info, e.g.:
1973

1974
        ::
1975

1976
            {'distro': 'Ubuntu\n',
1977
             'distro_ver': '14.04\n',
1978
             'machine': 'x86_64',
1979
             'node': 'pwnable.kr',
1980
             'processor': 'x86_64',
1981
             'release': '3.11.0-12-generic',
1982
             'system': 'linux',
1983
             'version': '#19-ubuntu smp wed oct 9 16:20:46 utc 2013'}
1984
        """
1985
        if self._platform_info:
1✔
1986
            return
1✔
1987

1988
        def preexec():
1✔
1989
            import platform
×
1990
            print('\n'.join(platform.uname()))
×
1991

1992
        with context.quiet:
1✔
1993
            with self.process('true', preexec_fn=preexec) as io:
1✔
1994

1995
                self._platform_info = {
1✔
1996
                    'system': io.recvline().lower().strip().decode(),
1997
                    'node': io.recvline().lower().strip().decode(),
1998
                    'release': io.recvline().lower().strip().decode(),
1999
                    'version': io.recvline().lower().strip().decode(),
2000
                    'machine': io.recvline().lower().strip().decode(),
2001
                    'processor': io.recvline().lower().strip().decode(),
2002
                    'distro': 'Unknown',
2003
                    'distro_ver': ''
2004
                }
2005

2006
            try:
1✔
2007
                if not self.which('lsb_release'):
1!
2008
                    return
×
2009

2010
                with self.process(['lsb_release', '-irs']) as io:
1✔
2011
                    lsb_info = io.recvall().strip().decode()
1✔
2012
                    self._platform_info['distro'], self._platform_info['distro_ver'] = lsb_info.split()
1✔
2013
            except Exception:
×
2014
                pass
×
2015

2016
    @property
1✔
2017
    def os(self):
1✔
2018
        """:class:`str`: Operating System of the remote machine."""
2019
        try:
1✔
2020
            self._init_remote_platform_info()
1✔
2021
            with context.local(os=self._platform_info['system']):
1✔
2022
                return context.os
1✔
2023
        except Exception:
×
2024
            return "Unknown"
×
2025

2026

2027
    @property
1✔
2028
    def arch(self):
1✔
2029
        """:class:`str`: CPU Architecture of the remote machine."""
2030
        try:
1✔
2031
            self._init_remote_platform_info()
1✔
2032
            with context.local(arch=self._platform_info['machine']):
1✔
2033
                return context.arch
1✔
2034
        except Exception:
×
2035
            return "Unknown"
×
2036

2037
    @property
1✔
2038
    def bits(self):
1✔
2039
        """:class:`str`: Pointer size of the remote machine."""
2040
        try:
×
2041
            with context.local():
×
2042
                context.clear()
×
2043
                context.arch = self.arch
×
2044
                return context.bits
×
2045
        except Exception:
×
2046
            return context.bits
×
2047

2048
    @property
1✔
2049
    def version(self):
1✔
2050
        """:class:`tuple`: Kernel version of the remote machine."""
2051
        try:
1✔
2052
            self._init_remote_platform_info()
1✔
2053
            vers = self._platform_info['release']
1✔
2054

2055
            # 3.11.0-12-generic
2056
            expr = r'([0-9]+\.?)+'
1✔
2057

2058
            vers = re.search(expr, vers).group()
1✔
2059
            return tuple(map(int, vers.split('.')))
1✔
2060

2061
        except Exception:
×
2062
            return (0,0,0)
×
2063

2064
    @property
1✔
2065
    def distro(self):
1✔
2066
        """:class:`tuple`: Linux distribution name and release."""
2067
        try:
1✔
2068
            self._init_remote_platform_info()
1✔
2069
            return (self._platform_info['distro'], self._platform_info['distro_ver'])
1✔
2070
        except Exception:
×
2071
            return ("Unknown", "Unknown")
×
2072

2073
    @property
1✔
2074
    def aslr(self):
1✔
2075
        """:class:`bool`: Whether ASLR is enabled on the system.
2076

2077
        Example:
2078

2079
            >>> s = ssh("travis", "example.pwnme")
2080
            >>> s.aslr
2081
            True
2082
        """
2083
        if self._aslr is None:
1!
2084
            if self.os != 'linux':
1!
2085
                self.warn_once("Only Linux is supported for ASLR checks.")
×
2086
                self._aslr = False
×
2087

2088
            else:
2089
                with context.quiet:
1✔
2090
                    rvs = self.read('/proc/sys/kernel/randomize_va_space')
1✔
2091

2092
                self._aslr = not rvs.startswith(b'0')
1✔
2093

2094
        return self._aslr
1✔
2095

2096
    @property
1✔
2097
    def aslr_ulimit(self):
1✔
2098
        """:class:`bool`: Whether the entropy of 32-bit processes can be reduced with ulimit."""
2099
        import pwnlib.elf.elf
1✔
2100
        import pwnlib.shellcraft
1✔
2101

2102
        if self._aslr_ulimit is not None:
1!
2103
            return self._aslr_ulimit
×
2104

2105
        # This test must run a 32-bit binary, fix the architecture
2106
        arch = {
1✔
2107
            'amd64': 'i386',
2108
            'aarch64': 'arm'
2109
        }.get(self.arch, self.arch)
2110

2111
        with context.local(arch=arch, bits=32, os=self.os, aslr=True):
1!
2112
            with context.quiet:
1✔
2113
                try:
1✔
2114
                    sc = pwnlib.shellcraft.cat('/proc/self/maps') \
1✔
2115
                       + pwnlib.shellcraft.exit(0)
2116

2117
                    elf = pwnlib.elf.elf.ELF.from_assembly(sc, shared=True)
1✔
2118
                except Exception:
×
2119
                    self.warn_once("Can't determine ulimit ASLR status")
×
2120
                    self._aslr_ulimit = False
×
2121
                    return self._aslr_ulimit
×
2122

2123
                def preexec():
1✔
2124
                    import resource
×
2125
                    try:
×
2126
                        resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
2127
                    except Exception:
×
2128
                        pass
×
2129

2130
                # Move to a new temporary directory
2131
                cwd = self.cwd
1✔
2132
                tmp = self.set_working_directory()
1✔
2133

2134
                try:
1✔
2135
                    self.upload(elf.path, './aslr-test')
1✔
2136
                except IOError:
×
2137
                    self.warn_once("Couldn't check ASLR ulimit trick")
×
2138
                    self._aslr_ulimit = False
×
2139
                    return False
×
2140

2141
                self.process(['chmod', '+x', './aslr-test']).wait()
1✔
2142
                maps = self.process(['./aslr-test'], preexec_fn=preexec).recvall()
1✔
2143

2144
                # Move back to the old directory
2145
                self.cwd = cwd
1✔
2146

2147
                # Clean up the files
2148
                self.process(['rm', '-rf', tmp]).wait()
1✔
2149

2150
        # Check for 555555000 (1/3 of the address space for PAE)
2151
        # and for 40000000 (1/3 of the address space with 3BG barrier)
2152
        self._aslr_ulimit = bool(b'55555000' in maps or b'40000000' in maps)
1✔
2153

2154
        return self._aslr_ulimit
1✔
2155

2156
    def _cpuinfo(self):
1✔
2157
        if self._cpuinfo_cache is None:
1✔
2158
            with context.quiet:
1✔
2159
                try:
1✔
2160
                    self._cpuinfo_cache = self.download_data('/proc/cpuinfo', fingerprint=False)
1✔
NEW
2161
                except PwnlibException:
×
NEW
2162
                    self._cpuinfo_cache = b''
×
2163
        return self._cpuinfo_cache
1✔
2164

2165
    @property
1✔
2166
    def user_shstk(self):
1✔
2167
        """:class:`bool`: Whether userspace shadow stack is supported on the system.
2168

2169
        Example:
2170

2171
            >>> s = ssh("travis", "example.pwnme")
2172
            >>> s.user_shstk
2173
            False
2174
        """
2175
        if self._user_shstk is None:
1!
2176
            if self.os != 'linux':
1!
NEW
2177
                self.warn_once("Only Linux is supported for userspace shadow stack checks.")
×
NEW
2178
                self._user_shstk = False
×
2179

2180
            else:
2181
                cpuinfo = self._cpuinfo()
1✔
2182

2183
                self._user_shstk = b' user_shstk' in cpuinfo
1✔
2184
        return self._user_shstk
1✔
2185

2186
    @property
1✔
2187
    def ibt(self):
1✔
2188
        """:class:`bool`: Whether kernel indirect branch tracking is supported on the system.
2189

2190
        Example:
2191

2192
            >>> s = ssh("travis", "example.pwnme")
2193
            >>> s.ibt
2194
            False
2195
        """
2196
        if self._ibt is None:
1!
2197
            if self.os != 'linux':
1!
NEW
2198
                self.warn_once("Only Linux is supported for kernel indirect branch tracking checks.")
×
NEW
2199
                self._ibt = False
×
2200

2201
            else:
2202
                cpuinfo = self._cpuinfo()
1✔
2203

2204
                self._ibt = b' ibt ' in cpuinfo or b' ibt\n' in cpuinfo
1✔
2205
        return self._ibt
1✔
2206

2207
    def _checksec_cache(self, value=None):
1✔
2208
        path = self._get_cachefile('%s-%s' % (self.host, self.port))
1✔
2209

2210
        if value is not None:
1✔
2211
            with open(path, 'w+') as f:
1✔
2212
                f.write(value)
1✔
2213
        elif os.path.exists(path):
1✔
2214
            with open(path, 'r+') as f:
1✔
2215
                return f.read()
1✔
2216

2217
    def checksec(self, banner=True):
1✔
2218
        """checksec()
2219

2220
        Prints a helpful message about the remote system.
2221

2222
        Arguments:
2223
            banner(bool): Whether to print the path to the ELF binary.
2224
        """
2225
        cached = self._checksec_cache()
1✔
2226
        if cached:
1✔
2227
            return cached
1✔
2228

2229
        red    = text.red
1✔
2230
        green  = text.green
1✔
2231
        yellow = text.yellow
1✔
2232

2233
        res = [
1✔
2234
            "%s@%s:" % (self.user, self.host),
2235
            "Distro".ljust(10) + ' '.join(self.distro),
2236
            "OS:".ljust(10) + self.os,
2237
            "Arch:".ljust(10) + self.arch,
2238
            "Version:".ljust(10) + '.'.join(map(str, self.version)),
2239

2240
            "ASLR:".ljust(10) + {
2241
                True: green("Enabled"),
2242
                False: red("Disabled")
2243
            }[self.aslr],
2244
            "SHSTK:".ljust(10) + {
2245
                True: green("Enabled"),
2246
                False: red("Disabled")
2247
            }[self.user_shstk],
2248
            "IBT:".ljust(10) + {
2249
                True: green("Enabled"),
2250
                False: red("Disabled")
2251
            }[self.ibt],
2252
        ]
2253

2254
        if self.aslr_ulimit:
1!
2255
            res += [ "Note:".ljust(10) + red("Susceptible to ASLR ulimit trick (CVE-2016-3672)")]
×
2256

2257
        cached = '\n'.join(res)
1✔
2258
        self._checksec_cache(cached)
1✔
2259
        return cached
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc