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

Gallopsled / pwntools / 5707294467

pending completion
5707294467

push

github-actions

peace-maker
Rename `wd` parameter to `cwd` in `ssh.system`

The current working directory should be called the same everywhere.
The `wd` argument is deprecated and `cwd` takes precedence.

4561 of 7234 branches covered (63.05%)

15 of 15 new or added lines in 1 file covered. (100.0%)

12780 of 17186 relevant lines covered (74.36%)

0.74 hits per line

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

66.74
/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.log import Logger
1✔
21
from pwnlib.log import getLogger
1✔
22
from pwnlib.term import text
1✔
23
from pwnlib.timeout import Timeout
1✔
24
from pwnlib.tubes.sock import sock
1✔
25
from pwnlib.util import hashes
1✔
26
from pwnlib.util import misc
1✔
27
from pwnlib.util import packing
1✔
28
from pwnlib.util import safeeval
1✔
29
from pwnlib.util.sh_string import sh_string
1✔
30

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

38
class ssh_channel(sock):
1✔
39

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

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

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

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

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

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

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

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

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

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

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

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

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

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

100

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

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

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

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

125
                self.resizer = resizer
1✔
126
                term.term.on_winch.append(self.resizer)
1✔
127
            else:
128
                self.resizer = None
1✔
129

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

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

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

141
            h.success()
1✔
142

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

146
        Kills the process.
147
        """
148

149
        self.close()
×
150

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

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

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

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

172
        return data
1✔
173

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

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

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

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

191
        return self.returncode
1✔
192

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

333

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

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

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

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

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

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

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

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

371

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

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

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

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

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

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

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

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

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

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

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

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

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

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

434

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

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

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

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

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

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

467
            h.success()
1✔
468

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

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

475

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

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

483
        self.host = parent.host
1✔
484

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

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

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

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

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

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

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

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

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

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

536

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

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

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

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

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

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

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

558
    _cwd = '.'
1✔
559

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

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

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

584
        Example proxying:
585

586
        .. doctest::
587
           :skipif: True
588

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

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

601

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

612
        # Deferred attributes
613
        self._platform_info = {}
1✔
614
        self._aslr = None
1✔
615
        self._aslr_ulimit = None
1✔
616

617
        misc.mkdir_p(self._cachedir)
1✔
618

619
        import paramiko
1✔
620

621
        # Make a basic attempt to parse the ssh_config file
622
        try:
1✔
623
            config_file = os.path.expanduser('~/.ssh/config')
1✔
624

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

640
        keyfiles = [os.path.expanduser(keyfile)] if keyfile else []
1✔
641

642
        msg = 'Connecting to %s on port %d' % (host, port)
1✔
643
        with self.waitfor(msg) as h:
1✔
644
            self.client = paramiko.SSHClient()
1✔
645
            self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1✔
646

647
            if not ignore_config:
1!
648
                known_hosts = os.path.expanduser('~/.ssh/known_hosts')
1✔
649
                if os.path.exists(known_hosts):
1!
650
                    self.client.load_host_keys(known_hosts)
1✔
651

652
            has_proxy = bool(proxy_sock or proxy_command)
1✔
653
            if has_proxy:
1!
654
                if 'ProxyCommand' not in dir(paramiko):
×
655
                    self.error('This version of paramiko does not support proxies.')
×
656

657
                if proxy_sock and proxy_command:
×
658
                    self.error('Cannot have both a proxy command and a proxy sock')
×
659

660
                if proxy_command:
×
661
                    proxy_sock = paramiko.ProxyCommand(proxy_command)
×
662
            else:
663
                proxy_sock = None
1✔
664

665
            try:
1✔
666
                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✔
667
            except paramiko.BadHostKeyException as e:
×
668
                self.error("Remote host %(host)s is using a different key than stated in known_hosts\n"
×
669
                           "    To remove the existing entry from your known_hosts and trust the new key, run the following commands:\n"
670
                           "        $ ssh-keygen -R %(host)s\n"
671
                           "        $ ssh-keygen -R [%(host)s]:%(port)s" % locals())
672

673
            self.transport = self.client.get_transport()
1✔
674
            self.transport.use_compression(True)
1✔
675

676
            h.success()
1✔
677

678
        if self.raw:
1!
679
            return
×
680

681
        self._tried_sftp = False
1✔
682

683
        if self.sftp:
1!
684
            with context.quiet:
1✔
685
                self.cwd = packing._decode(self.pwd())
1✔
686
        else:
687
            self.cwd = '.'
×
688

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

697
        try:
1✔
698
            self.info_once(self.checksec())
1✔
699
        except Exception:
×
700
            self.warn_once("Couldn't check security settings on %r" % self.host)
×
701

702
    def __repr__(self):
1✔
703
        return "{}(user={!r}, host={!r})".format(self.__class__.__name__, self.user, self.host)
1✔
704

705
    @property
1✔
706
    def cwd(self):
1✔
707
        return self._cwd
1✔
708

709
    @cwd.setter
1✔
710
    def cwd(self, cwd):
1✔
711
        self._cwd = cwd
1✔
712
        if self.sftp:
1!
713
            self.sftp.chdir(cwd)
1✔
714

715
    @property
1✔
716
    def sftp(self):
1✔
717
        if not self._tried_sftp:
1✔
718
            try:
1✔
719
                self._sftp = self.transport.open_sftp_client()
1✔
720
            except Exception:
×
721
                self._sftp = None
×
722

723
        self._tried_sftp = True
1✔
724
        return self._sftp
1✔
725

726
    @sftp.setter
1✔
727
    def sftp(self, value):
1✔
728
        self._sftp = value
×
729
        self._tried_sftp = True
×
730

731
    def __enter__(self, *a):
1✔
732
        return self
×
733

734
    def __exit__(self, *a, **kw):
1✔
735
        self.close()
×
736

737
    def shell(self, shell = None, tty = True, timeout = Timeout.default):
1✔
738
        """shell(shell = None, tty = True, timeout = Timeout.default) -> ssh_channel
739

740
        Open a new channel with a shell inside.
741

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

747
        Returns:
748
            Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
749

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

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

766
        To achieve this, a Python script is created to call ``os.execve``
767
        with the appropriate arguments.
768

769
        As an added bonus, the ``ssh_channel`` object returned has a
770
        ``pid`` property for the process pid.
771

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

821
        Returns:
822
            A new SSH channel, or a path to a script if ``run=False``.
823

824
        Notes:
825
            Requires Python on the remote server.
826

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

862
            >>> s.process('false', preexec_fn=1234)
863
            Traceback (most recent call last):
864
            ...
865
            PwnlibException: preexec_fn must be a function
866

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

872
            >>> def uses_globals():
873
            ...     foo = bar
874
            >>> print(s.process('false', preexec_fn=uses_globals).recvall().strip().decode()) # doctest: +ELLIPSIS
875
            Traceback (most recent call last):
876
            ...
877
            NameError: ...name 'bar' is not defined
878

879
            >>> s.process('echo hello', shell=True).recvall()
880
            b'hello\n'
881

882
            >>> io = s.process(['cat'], timeout=5)
883
            >>> io.recvline()
884
            b''
885
        """
886
        if not argv and not executable:
1!
887
            self.error("Must specify argv or executable")
×
888

889
        aslr      = aslr if aslr is not None else context.aslr
1✔
890

891
        argv, env = misc.normalize_argv_env(argv, env, self)
1✔
892

893
        if shell:
1✔
894
            if len(argv) != 1:
1!
895
                self.error('Cannot provide more than 1 argument if shell=True')
×
896
            argv = [bytearray(b'/bin/sh'), bytearray(b'-c')] + argv
1✔
897

898
        executable = executable or argv[0]
1✔
899
        cwd        = cwd or self.cwd
1✔
900

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

906
        # Allow passing in sys.stdin/stdout/stderr objects
907
        handles = {sys.stdin: 0, sys.stdout:1, sys.stderr:2}
1✔
908
        stdin  = handles.get(stdin, stdin)
1✔
909
        stdout = handles.get(stdout, stdout)
1✔
910
        stderr = handles.get(stderr, stderr)
1✔
911

912
        # Allow the user to provide a self-contained function to run
913
        def func(): pass
1!
914
        func      = preexec_fn or func
1✔
915
        func_args = preexec_args
1✔
916

917
        if not isinstance(func, types.FunctionType):
1✔
918
            self.error("preexec_fn must be a function")
1✔
919

920
        func_name = func.__name__
1✔
921
        if func_name == (lambda: 0).__name__:
1!
922
            self.error("preexec_fn cannot be a lambda")
1✔
923

924
        func_src  = inspect.getsource(func).strip()
1✔
925
        setuid = True if setuid is None else bool(setuid)
1✔
926

927
        script = r"""
1✔
928
#!/usr/bin/env python
929
import os, sys, ctypes, resource, platform, stat
930
from collections import OrderedDict
931
try:
932
    integer_types = int, long
933
except NameError:
934
    integer_types = int,
935
exe   = bytes(%(executable)r)
936
argv  = [bytes(a) for a in %(argv)r]
937
env   = %(env)r
938

939
os.chdir(%(cwd)r)
940

941
environ = getattr(os, 'environb', os.environ)
942

943
if env is not None:
944
    env = OrderedDict((bytes(k), bytes(v)) for k,v in env)
945
    os.environ.clear()
946
    environ.update(env)
947
else:
948
    env = os.environ
949

950
def is_exe(path):
951
    return os.path.isfile(path) and os.access(path, os.X_OK)
952

953
PATH = environ.get(b'PATH',b'').split(os.pathsep.encode())
954

955
if os.path.sep.encode() not in exe and not is_exe(exe):
956
    for path in PATH:
957
        test_path = os.path.join(path, exe)
958
        if is_exe(test_path):
959
            exe = test_path
960
            break
961

962
if not is_exe(exe):
963
    sys.stderr.write('3\n')
964
    sys.stderr.write("{!r} is not executable or does not exist in $PATH: {!r}".format(exe,PATH))
965
    sys.exit(-1)
966

967
if not %(setuid)r:
968
    PR_SET_NO_NEW_PRIVS = 38
969
    result = ctypes.CDLL('libc.so.6').prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
970

971
    if result != 0:
972
        sys.stdout.write('3\n')
973
        sys.stdout.write("Could not disable setuid: prctl(PR_SET_NO_NEW_PRIVS) failed")
974
        sys.exit(-1)
975

976
try:
977
    PR_SET_PTRACER = 0x59616d61
978
    PR_SET_PTRACER_ANY = -1
979
    ctypes.CDLL('libc.so.6').prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0)
980
except Exception:
981
    pass
982

983
# Determine what UID the process will execute as
984
# This is used for locating apport core dumps
985
suid = os.getuid()
986
sgid = os.getgid()
987
st = os.stat(exe)
988
if %(setuid)r:
989
    if (st.st_mode & stat.S_ISUID):
990
        suid = st.st_uid
991
    if (st.st_mode & stat.S_ISGID):
992
        sgid = st.st_gid
993

994
if sys.argv[-1] == 'check':
995
    sys.stdout.write("1\n")
996
    sys.stdout.write(str(os.getpid()) + "\n")
997
    sys.stdout.write(str(os.getuid()) + "\n")
998
    sys.stdout.write(str(os.getgid()) + "\n")
999
    sys.stdout.write(str(suid) + "\n")
1000
    sys.stdout.write(str(sgid) + "\n")
1001
    getattr(sys.stdout, 'buffer', sys.stdout).write(os.path.realpath(exe) + b'\x00')
1002
    sys.stdout.flush()
1003

1004
for fd, newfd in {0: %(stdin)r, 1: %(stdout)r, 2:%(stderr)r}.items():
1005
    if newfd is None:
1006
        os.close(fd)
1007
    elif isinstance(newfd, (str, bytes)):
1008
        newfd = os.open(newfd, os.O_RDONLY if fd == 0 else (os.O_RDWR|os.O_CREAT))
1009
        os.dup2(newfd, fd)
1010
        os.close(newfd)
1011
    elif isinstance(newfd, integer_types) and newfd != fd:
1012
        os.dup2(fd, newfd)
1013

1014
if not %(aslr)r:
1015
    if platform.system().lower() == 'linux' and %(setuid)r is not True:
1016
        ADDR_NO_RANDOMIZE = 0x0040000
1017
        ctypes.CDLL('libc.so.6').personality(ADDR_NO_RANDOMIZE)
1018

1019
    resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
1020

1021
# Attempt to dump ALL core file regions
1022
try:
1023
    with open('/proc/self/coredump_filter', 'w') as core_filter:
1024
        core_filter.write('0x3f\n')
1025
except Exception:
1026
    pass
1027

1028
# Assume that the user would prefer to have core dumps.
1029
try:
1030
    resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
1031
except Exception:
1032
    pass
1033

1034
%(func_src)s
1035
%(func_name)s(*%(func_args)r)
1036

1037
os.execve(exe, argv, env)
1038
""" % locals()  # """
1039

1040
        script = script.strip()
1✔
1041

1042
        self.debug("Created execve script:\n" + script)
1✔
1043

1044
        if not run:
1!
1045
            with context.local(log_level='error'):
×
1046
                tmpfile = self.mktemp('-t', 'pwnlib-execve-XXXXXXXXXX')
×
1047
                self.chmod('+x', tmpfile)
×
1048

1049
            self.info("Uploading execve script to %r" % tmpfile)
×
1050
            self.upload_data(script, tmpfile)
×
1051
            return tmpfile
×
1052

1053
        if self.isEnabledFor(logging.DEBUG):
1!
1054
            execve_repr = "execve(%r, %s, %s)" % (executable,
×
1055
                                                  argv,
1056
                                                  'os.environ'
1057
                                                  if (env in (None, os.environ))
1058
                                                  else env)
1059
            # Avoid spamming the screen
1060
            if self.isEnabledFor(logging.DEBUG) and len(execve_repr) > 512:
×
1061
                execve_repr = execve_repr[:512] + '...'
×
1062
        else:
1063
            execve_repr = repr(executable)
1✔
1064

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

1067
        if timeout == Timeout.default:
1✔
1068
            timeout = self.timeout
1✔
1069

1070
        with self.progress(msg) as h:
1!
1071

1072
            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✔
1073
            with context.quiet:
1✔
1074
                python = ssh_process(self, script, tty=True, cwd=cwd, raw=True, level=self.level, timeout=timeout)
1✔
1075

1076
            try:
1✔
1077
                python.recvline_contains(b'PWNTOOLS')        # Magic flag so that any sh/bash initialization errors are swallowed
1✔
1078
                python.recvline()                           # Python interpreter that was selected
1✔
1079
                result = safeeval.const(python.recvline())  # Status flag from the Python script
1✔
1080
            except (EOFError, ValueError):
×
1081
                h.failure("Process creation failed")
×
1082
                self.warn_once('Could not find a Python interpreter on %s\n' % self.host
×
1083
                               + "Use ssh.run() instead of ssh.process()\n"
1084
                                 "The original error message:\n"
1085
                               + python.recvall().decode())
1086
                return None
×
1087

1088
            # If an error occurred, try to grab as much output
1089
            # as we can.
1090
            if result != 1:
1!
1091
                error_message = python.recvrepeat(timeout=1)
×
1092

1093
            if result == 0:
1!
1094
                self.error("%r does not exist or is not executable" % executable)
×
1095
            elif result == 3:
1!
1096
                self.error("%r" % error_message)
×
1097
            elif result == 2:
1!
1098
                self.error("python is not installed on the remote system %r" % self.host)
×
1099
            elif result != 1:
1!
1100
                h.failure("something bad happened:\n%s" % error_message)
×
1101

1102
            python.pid  = safeeval.const(python.recvline())
1✔
1103
            python.uid  = safeeval.const(python.recvline())
1✔
1104
            python.gid  = safeeval.const(python.recvline())
1✔
1105
            python.suid = safeeval.const(python.recvline())
1✔
1106
            python.sgid = safeeval.const(python.recvline())
1✔
1107
            python.argv = argv
1✔
1108
            python.executable = packing._decode(python.recvuntil(b'\x00')[:-1])
1✔
1109

1110
            h.success('pid %i' % python.pid)
1✔
1111

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

1117
            if self.aslr_ulimit:
×
1118
                message += "Unlimited stack size should de-randomize shared libraries."
×
1119

1120
            self.warn_once(message)
×
1121

1122
        elif not aslr:
1!
1123
            self.warn_once("ASLR is disabled for %r!" % python.executable)
×
1124

1125
        return python
1✔
1126

1127
    def which(self, program):
1✔
1128
        """which(program) -> str
1129

1130
        Minor modification to just directly invoking ``which`` on the remote
1131
        system which adds the current working directory to the end of ``$PATH``.
1132
        """
1133
        # If name is a path, do not attempt to resolve it.
1134
        if os.path.sep in program:
1✔
1135
            return program
1✔
1136

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

1139
        if ('/%s' % program) not in result:
1✔
1140
            return None
1✔
1141

1142
        return result
1✔
1143

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

1147
        Open a new channel with a specific process inside. If `tty` is True,
1148
        then a TTY is requested on the remote server.
1149

1150
        If `raw` is True, terminal control codes are ignored and input is not
1151
        echoed back.
1152

1153
        Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
1154

1155
        Examples:
1156
            >>> s =  ssh(host='example.pwnme')
1157
            >>> py = s.system('python3 -i')
1158
            >>> _ = py.recvuntil(b'>>> ')
1159
            >>> py.sendline(b'print(2+2)')
1160
            >>> py.sendline(b'exit()')
1161
            >>> print(repr(py.recvline()))
1162
            b'4\n'
1163
            >>> s.system('env | grep -a AAAA', env={'AAAA': b'\x90'}).recvall()
1164
            b'AAAA=\x90\n'
1165
            >>> io = s.system('pwd', cwd='/tmp')
1166
            >>> io.recvall()
1167
            b'/tmp\n'
1168
            >>> io.cwd
1169
            '/tmp'
1170
        """
1171
        if wd is not None:
1!
1172
            self.warning_once("The 'wd' argument to ssh.system() is deprecated.  Use 'cwd' instead.")
×
1173
            if cwd is None:
×
1174
                cwd = wd
×
1175
        if cwd is None:
1✔
1176
            cwd = self.cwd
1✔
1177

1178
        if timeout is None:
1✔
1179
            timeout = self.timeout
1✔
1180

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

1183
    #: Backward compatibility.  Use :meth:`system`
1184
    run = system
1✔
1185

1186
    def getenv(self, variable, **kwargs):
1✔
1187
        """Retrieve the address of an environment variable on the remote
1188
        system.
1189

1190
        Note:
1191

1192
            The exact address will differ based on what other environment
1193
            variables are set, as well as argv[0].  In order to ensure that
1194
            the path is *exactly* the same, it is recommended to invoke the
1195
            process with ``argv=[]``.
1196
        """
1197
        script = '''
×
1198
from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r))
1199
''' % variable
1200

1201
        with context.local(log_level='error'):
×
1202
            python = self.which('python') or self.which('python2.7') or self.which('python3')
×
1203

1204
            if not python:
×
1205
                self.error("Python is not installed on the remote system.")
×
1206

1207
            io = self.process(['','-c', script.strip()], executable=python, **kwargs)
×
1208
            result = io.recvall()
×
1209

1210
        try:
×
1211
            return int(result) & context.mask
×
1212
        except ValueError:
×
1213
            self.exception("Could not look up environment variable %r" % variable)
×
1214

1215

1216

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

1220
        Run a command on the remote server and return a tuple with
1221
        (data, exit_status). If `tty` is True, then the command is run inside
1222
        a TTY on the remote server.
1223

1224
        Examples:
1225
            >>> s =  ssh(host='example.pwnme')
1226
            >>> print(s.run_to_end('echo Hello; exit 17'))
1227
            (b'Hello\n', 17)
1228
            """
1229

1230
        if wd is not None:
1✔
1231
            self.warning_once("The 'wd' argument to ssh.run_to_end() is deprecated.  Use 'cwd' instead.")
1✔
1232
            if cwd is None:
1!
1233
                cwd = wd
1✔
1234

1235
        with context.local(log_level = 'ERROR'):
1✔
1236
            c = self.run(process, tty, cwd = cwd, env = env, timeout = Timeout.default)
1✔
1237
            data = c.recvall()
1✔
1238
            retcode = c.wait()
1✔
1239
            c.close()
1✔
1240
            return data, retcode
1✔
1241

1242
    def connect_remote(self, host, port, timeout = Timeout.default):
1✔
1243
        r"""connect_remote(host, port, timeout = Timeout.default) -> ssh_connecter
1244

1245
        Connects to a host through an SSH connection. This is equivalent to
1246
        using the ``-L`` flag on ``ssh``.
1247

1248
        Returns a :class:`pwnlib.tubes.ssh.ssh_connecter` object.
1249

1250
        Examples:
1251
            >>> from pwn import *
1252
            >>> l = listen()
1253
            >>> s =  ssh(host='example.pwnme')
1254
            >>> a = s.connect_remote(s.host, l.lport)
1255
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1256
            >>> a.sendline(b'Hello')
1257
            >>> print(repr(b.recvline()))
1258
            b'Hello\n'
1259
        """
1260

1261
        return ssh_connecter(self, host, port, timeout, level=self.level)
1✔
1262

1263
    remote = connect_remote
1✔
1264

1265
    def listen_remote(self, port = 0, bind_address = '', timeout = Timeout.default):
1✔
1266
        r"""listen_remote(port = 0, bind_address = '', timeout = Timeout.default) -> ssh_connecter
1267

1268
        Listens remotely through an SSH connection. This is equivalent to
1269
        using the ``-R`` flag on ``ssh``.
1270

1271
        Returns a :class:`pwnlib.tubes.ssh.ssh_listener` object.
1272

1273
        Examples:
1274

1275
            >>> from pwn import *
1276
            >>> s =  ssh(host='example.pwnme')
1277
            >>> l = s.listen_remote()
1278
            >>> a = remote(s.host, l.port)
1279
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1280
            >>> a.sendline(b'Hello')
1281
            >>> print(repr(b.recvline()))
1282
            b'Hello\n'
1283
        """
1284

1285
        return ssh_listener(self, bind_address, port, timeout, level=self.level)
1✔
1286

1287
    listen = listen_remote
1✔
1288

1289
    def __getitem__(self, attr):
1✔
1290
        """Permits indexed access to run commands over SSH
1291

1292
        Examples:
1293

1294
            >>> s =  ssh(host='example.pwnme')
1295
            >>> print(repr(s['echo hello']))
1296
            b'hello'
1297
        """
1298
        return self.run(attr).recvall().strip()
1✔
1299

1300
    def __call__(self, attr):
1✔
1301
        """Permits function-style access to run commands over SSH
1302

1303
        Examples:
1304

1305
            >>> s =  ssh(host='example.pwnme')
1306
            >>> print(repr(s('echo hello')))
1307
            b'hello'
1308
        """
1309
        return self.run(attr).recvall().strip()
1✔
1310

1311
    def __getattr__(self, attr):
1✔
1312
        """Permits member access to run commands over SSH
1313

1314
        Examples:
1315

1316
            >>> s =  ssh(host='example.pwnme')
1317
            >>> s.echo('hello')
1318
            b'hello'
1319
            >>> s.whoami()
1320
            b'travis'
1321
            >>> s.echo(['huh','yay','args'])
1322
            b'huh yay args'
1323
        """
1324
        bad_attrs = [
1✔
1325
            'trait_names',          # ipython tab-complete
1326
        ]
1327

1328
        if attr in self.__dict__ \
1!
1329
        or attr in bad_attrs \
1330
        or attr.startswith('_'):
1331
            raise AttributeError
×
1332

1333
        @LocalContext
1✔
1334
        def runner(*args):
1✔
1335
            if len(args) == 1 and isinstance(args[0], (list, tuple)):
1✔
1336
                command = [attr]
1✔
1337
                command.extend(args[0])
1✔
1338
            else:
1339
                command = [attr]
1✔
1340
                command.extend(args)
1✔
1341
                command = b' '.join(packing._need_bytes(arg, min_wrong=0x80) for arg in command)
1✔
1342

1343
            return self.run(command).recvall().strip()
1✔
1344
        return runner
1✔
1345

1346
    def connected(self):
1✔
1347
        """Returns True if we are connected.
1348

1349
        Example:
1350

1351
            >>> s =  ssh(host='example.pwnme')
1352
            >>> s.connected()
1353
            True
1354
            >>> s.close()
1355
            >>> s.connected()
1356
            False
1357
        """
1358
        return bool(self.client and self.client.get_transport().is_active())
1✔
1359

1360
    def close(self):
1✔
1361
        """Close the connection."""
1362
        if self.client:
1!
1363
            self.client.close()
1✔
1364
            self.client = None
1✔
1365
            self.info("Closed connection to %r" % self.host)
1✔
1366

1367
    def _libs_remote(self, remote):
1✔
1368
        """Return a dictionary of the libraries used by a remote file."""
1369
        escaped_remote = sh_string(remote)
1✔
1370
        cmd = ''.join([
1✔
1371
            '(',
1372
            'ulimit -s unlimited;',
1373
            'ldd %s > /dev/null &&' % escaped_remote,
1374
            '(',
1375
            'LD_TRACE_LOADED_OBJECTS=1 %s||' % escaped_remote,
1376
            'ldd %s' % escaped_remote,
1377
            '))',
1378
            ' 2>/dev/null'
1379
        ])
1380
        data, status = self.run_to_end(cmd)
1✔
1381
        if status != 0:
1!
1382
            self.error('Unable to find libraries for %r' % remote)
×
1383
            return {}
×
1384

1385
        return misc.parse_ldd_output(packing._decode(data))
1✔
1386

1387
    def _get_fingerprint(self, remote):
1✔
1388
        cmd = '(sha256 || sha256sum || openssl sha256) 2>/dev/null < '
1✔
1389
        cmd = cmd + sh_string(remote)
1✔
1390

1391
        data, status = self.run_to_end(cmd)
1✔
1392

1393
        if status != 0:
1!
1394
            return None
×
1395

1396
        if not isinstance(data, str):
1✔
1397
            data = data.decode('ascii')
1✔
1398

1399
        data = re.search("([a-fA-F0-9]{64})",data).group()
1✔
1400
        return data
1✔
1401

1402
    def _get_cachefile(self, fingerprint):
1✔
1403
        return os.path.join(self._cachedir, fingerprint)
1✔
1404

1405
    def _verify_local_fingerprint(self, fingerprint):
1✔
1406
        if not set(fingerprint).issubset(string.hexdigits) or \
1!
1407
           len(fingerprint) != 64:
1408
            self.error('Invalid fingerprint %r' % fingerprint)
×
1409
            return False
×
1410

1411
        local = self._get_cachefile(fingerprint)
1✔
1412
        if not os.path.isfile(local):
1✔
1413
            return False
1✔
1414

1415
        if hashes.sha256filehex(local) == fingerprint:
1!
1416
            return True
1✔
1417
        else:
1418
            os.unlink(local)
×
1419
            return False
×
1420

1421
    def _download_raw(self, remote, local, h):
1✔
1422
        def update(has, total):
1✔
1423
            h.status("%s/%s" % (misc.size(has), misc.size(total)))
1✔
1424

1425
        if self.sftp:
1✔
1426
            try:
1✔
1427
                self.sftp.get(remote, local, update)
1✔
1428
                return
1✔
1429
            except IOError:
×
1430
                pass
×
1431

1432
        cmd = 'wc -c < ' + sh_string(remote)
1✔
1433
        total, exitcode = self.run_to_end(cmd)
1✔
1434

1435
        if exitcode != 0:
1!
1436
            h.failure("%r does not exist or is not accessible" % remote)
×
1437
            return
×
1438

1439
        total = int(total)
1✔
1440

1441
        with context.local(log_level = 'ERROR'):
1✔
1442
            cmd = 'cat < ' + sh_string(remote)
1✔
1443
            c = self.run(cmd)
1✔
1444
        data = b''
1✔
1445

1446
        while True:
1✔
1447
            try:
1✔
1448
                data += c.recv()
1✔
1449
            except EOFError:
1✔
1450
                break
1✔
1451
            update(len(data), total)
1✔
1452

1453
        result = c.wait()
1✔
1454
        if result != 0:
1!
1455
            h.failure('Could not download file %r (%r)' % (remote, result))
×
1456
            return
×
1457

1458
        with open(local, 'wb') as fd:
1✔
1459
            fd.write(data)
1✔
1460

1461
    def _download_to_cache(self, remote, p):
1✔
1462

1463
        with context.local(log_level='error'):
1✔
1464
            remote = self.readlink('-f',remote)
1✔
1465
        if not hasattr(remote, 'encode'):
1✔
1466
            remote = remote.decode('utf-8')
1✔
1467

1468
        fingerprint = self._get_fingerprint(remote)
1✔
1469
        if fingerprint is None:
1!
1470
            local = os.path.normpath(remote)
×
1471
            local = os.path.basename(local)
×
1472
            local += time.strftime('-%Y-%m-%d-%H:%M:%S')
×
1473
            local = os.path.join(self._cachedir, local)
×
1474

1475
            self._download_raw(remote, local, p)
×
1476
            return local
×
1477

1478
        local = self._get_cachefile(fingerprint)
1✔
1479

1480
        if self.cache and self._verify_local_fingerprint(fingerprint):
1✔
1481
            p.success('Found %r in ssh cache' % remote)
1✔
1482
        else:
1483
            self._download_raw(remote, local, p)
1✔
1484

1485
            if not self._verify_local_fingerprint(fingerprint):
1!
1486
                self.error('Could not download file %r', remote)
×
1487

1488
        return local
1✔
1489

1490
    def download_data(self, remote):
1✔
1491
        """Downloads a file from the remote server and returns it as a string.
1492

1493
        Arguments:
1494
            remote(str): The remote filename to download.
1495

1496

1497
        Examples:
1498
            >>> with open('/tmp/bar','w+') as f:
1499
            ...     _ = f.write('Hello, world')
1500
            >>> s =  ssh(host='example.pwnme',
1501
            ...         cache=False)
1502
            >>> s.download_data('/tmp/bar')
1503
            b'Hello, world'
1504
            >>> s._sftp = None
1505
            >>> s._tried_sftp = True
1506
            >>> s.download_data('/tmp/bar')
1507
            b'Hello, world'
1508

1509
        """
1510
        with self.progress('Downloading %r' % remote) as p:
1✔
1511
            with open(self._download_to_cache(remote, p), 'rb') as fd:
1✔
1512
                return fd.read()
1✔
1513

1514
    def download_file(self, remote, local = None):
1✔
1515
        """Downloads a file from the remote server.
1516

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

1520
        Arguments:
1521
            remote(str/bytes): The remote filename to download
1522
            local(str): The local filename to save it to. Default is to infer it from the remote filename.
1523
        
1524
        Examples:
1525
            >>> with open('/tmp/foobar','w+') as f:
1526
            ...     _ = f.write('Hello, world')
1527
            >>> s =  ssh(host='example.pwnme',
1528
            ...         cache=False)
1529
            >>> _ = s.set_working_directory(wd='/tmp')
1530
            >>> _ = s.download_file('foobar', 'barfoo')
1531
            >>> with open('barfoo','r') as f:
1532
            ...     print(f.read())
1533
            Hello, world
1534
        """
1535

1536

1537
        if not local:
1✔
1538
            local = os.path.basename(os.path.normpath(remote))
1✔
1539

1540
        with self.progress('Downloading %r to %r' % (remote, local)) as p:
1✔
1541
            local_tmp = self._download_to_cache(remote, p)
1✔
1542

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

1547
    def download_dir(self, remote=None, local=None, ignore_failed_read=False):
1✔
1548
        """Recursively downloads a directory from the remote server
1549

1550
        Arguments:
1551
            local: Local directory
1552
            remote: Remote directory
1553
        """
1554
        remote = packing._encode(remote or self.cwd)
×
1555

1556
        if self.sftp:
×
1557
            remote = packing._encode(self.sftp.normalize(remote))
×
1558
        else:
1559
            with context.local(log_level='error'):
×
1560
                remote = self.system(b'readlink -f ' + sh_string(remote)).recvall().strip()
×
1561

1562
        local = local or '.'
×
1563
        local = os.path.expanduser(local)
×
1564

1565
        self.info("Downloading %r to %r" % (remote, local))
×
1566

1567
        if ignore_failed_read:
×
1568
            opts = b" --ignore-failed-read"
×
1569
        else:
1570
            opts = b""
×
1571
        with context.local(log_level='error'):
×
1572
            remote_tar = self.mktemp()
×
1573
            cmd = b'tar %s -C %s -czf %s .' % \
×
1574
                  (opts,
1575
                   sh_string(remote),
1576
                   sh_string(remote_tar))
1577
            tar = self.system(cmd)
×
1578

1579
            if 0 != tar.wait():
×
1580
                self.error("Could not create remote tar")
×
1581

1582
            local_tar = tempfile.NamedTemporaryFile(suffix='.tar.gz')
×
1583
            self.download_file(remote_tar, local_tar.name)
×
1584

1585
            # Delete temporary tarfile from remote host
1586
            if self.sftp:
×
1587
                self.unlink(remote_tar)
×
1588
            else:
1589
                self.system(b'rm ' + sh_string(remote_tar)).wait()
×
1590
            tar = tarfile.open(local_tar.name)
×
1591
            tar.extractall(local)
×
1592

1593

1594
    def upload_data(self, data, remote):
1✔
1595
        """Uploads some data into a file on the remote server.
1596

1597
        Arguments:
1598
            data(str): The data to upload.
1599
            remote(str): The filename to upload it to.
1600

1601
        Example:
1602
            >>> s =  ssh(host='example.pwnme')
1603
            >>> s.upload_data(b'Hello, world', '/tmp/upload_foo')
1604
            >>> print(open('/tmp/upload_foo').read())
1605
            Hello, world
1606
            >>> s._sftp = False
1607
            >>> s._tried_sftp = True
1608
            >>> s.upload_data(b'Hello, world', '/tmp/upload_bar')
1609
            >>> print(open('/tmp/upload_bar').read())
1610
            Hello, world
1611
        """
1612
        data = packing._need_bytes(data)
1✔
1613
        # If a relative path was provided, prepend the cwd
1614
        if os.path.normpath(remote) == os.path.basename(remote):
1✔
1615
            remote = os.path.join(self.cwd, remote)
1✔
1616

1617
        if self.sftp:
1✔
1618
            with tempfile.NamedTemporaryFile() as f:
1✔
1619
                f.write(data)
1✔
1620
                f.flush()
1✔
1621
                self.sftp.put(f.name, remote)
1✔
1622
                return
1✔
1623

1624
        with context.local(log_level = 'ERROR'):
1✔
1625
            cmd = 'cat > ' + sh_string(remote)
1✔
1626
            s = self.run(cmd, tty=False)
1✔
1627
            s.send(data)
1✔
1628
            s.shutdown('send')
1✔
1629
            data   = s.recvall()
1✔
1630
            result = s.wait()
1✔
1631
            if result != 0:
1!
1632
                self.error("Could not upload file %r (%r)\n%s" % (remote, result, data))
×
1633

1634
    def upload_file(self, filename, remote = None):
1✔
1635
        """Uploads a file to the remote server. Returns the remote filename.
1636

1637
        Arguments:
1638
        filename(str): The local filename to download
1639
        remote(str): The remote filename to save it to. Default is to infer it from the local filename."""
1640

1641

1642
        if remote is None:
1✔
1643
            remote = os.path.normpath(filename)
1✔
1644
            remote = os.path.basename(remote)
1✔
1645
            remote = os.path.join(self.cwd, remote)
1✔
1646

1647
        with open(filename, 'rb') as fd:
1✔
1648
            data = fd.read()
1✔
1649

1650
        self.info("Uploading %r to %r" % (filename,remote))
1✔
1651
        self.upload_data(data, remote)
1✔
1652

1653
        return remote
1✔
1654

1655
    def upload_dir(self, local, remote=None):
1✔
1656
        """Recursively uploads a directory onto the remote server
1657

1658
        Arguments:
1659
            local: Local directory
1660
            remote: Remote directory
1661
        """
1662

1663
        remote    = packing._encode(remote or self.cwd)
×
1664

1665
        local     = os.path.expanduser(local)
×
1666
        dirname   = os.path.dirname(local)
×
1667
        basename  = os.path.basename(local)
×
1668

1669
        if not os.path.isdir(local):
×
1670
            self.error("%r is not a directory" % local)
×
1671

1672
        msg = "Uploading %r to %r" % (basename,remote)
×
1673
        with self.waitfor(msg):
×
1674
            # Generate a tarfile with everything inside of it
1675
            local_tar  = tempfile.mktemp()
×
1676
            with tarfile.open(local_tar, 'w:gz') as tar:
×
1677
                tar.add(local, basename)
×
1678

1679
            # Upload and extract it
1680
            with context.local(log_level='error'):
×
1681
                remote_tar = self.mktemp('--suffix=.tar.gz')
×
1682
                self.upload_file(local_tar, remote_tar)
×
1683

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

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

1690
    def upload(self, file_or_directory, remote=None):
1✔
1691
        """upload(file_or_directory, remote=None)
1692

1693
        Upload a file or directory to the remote host.
1694

1695
        Arguments:
1696
            file_or_directory(str): Path to the file or directory to download.
1697
            remote(str): Local path to store the data.
1698
                By default, uses the working directory.
1699
        """
1700
        if isinstance(file_or_directory, str):
1!
1701
            file_or_directory = os.path.expanduser(file_or_directory)
1✔
1702
            file_or_directory = os.path.expandvars(file_or_directory)
1✔
1703

1704
        if os.path.isfile(file_or_directory):
1!
1705
            return self.upload_file(file_or_directory, remote)
1✔
1706

1707
        if os.path.isdir(file_or_directory):
×
1708
            return self.upload_dir(file_or_directory, remote)
×
1709

1710
        self.error('%r does not exist' % file_or_directory)
×
1711

1712
    def download(self, file_or_directory, local=None):
1✔
1713
        """download(file_or_directory, local=None)
1714

1715
        Download a file or directory from the remote host.
1716

1717
        Arguments:
1718
            file_or_directory(str): Path to the file or directory to download.
1719
            local(str): Local path to store the data.
1720
                By default, uses the current directory.
1721
        
1722

1723
        Examples:
1724
            >>> with open('/tmp/foobar','w+') as f:
1725
            ...     _ = f.write('Hello, world')
1726
            >>> s =  ssh(host='example.pwnme',
1727
            ...         cache=False)
1728
            >>> _ = s.set_working_directory('/tmp')
1729
            >>> _ = s.download('foobar', 'barfoo')
1730
            >>> with open('barfoo','r') as f:
1731
            ...     print(f.read())
1732
            Hello, world
1733
        """
1734
        file_or_directory = packing._encode(file_or_directory)
1✔
1735
        with self.system(b'test -d ' + sh_string(file_or_directory)) as io:
1✔
1736
            is_dir = io.wait()
1✔
1737

1738
        if 0 == is_dir:
1!
1739
            self.download_dir(file_or_directory, local)
×
1740
        else:
1741
            self.download_file(file_or_directory, local)
1✔
1742

1743
    put = upload
1✔
1744
    get = download
1✔
1745

1746
    def unlink(self, file):
1✔
1747
        """unlink(file)
1748

1749
        Delete the file on the remote host
1750

1751
        Arguments:
1752
            file(str): Path to the file
1753
        """
1754
        if not self.sftp:
×
1755
            self.error("unlink() is only supported if SFTP is supported")
×
1756

1757
        return self.sftp.unlink(file)
×
1758

1759
    def libs(self, remote, directory = None):
1✔
1760
        """Downloads the libraries referred to by a file.
1761

1762
        This is done by running ldd on the remote server, parsing the output
1763
        and downloading the relevant files.
1764

1765
        The directory argument specified where to download the files. This defaults
1766
        to './$HOSTNAME' where $HOSTNAME is the hostname of the remote server."""
1767

1768
        libs = self._libs_remote(remote)
1✔
1769

1770
        remote = packing._decode(self.readlink('-f',remote).strip())
1✔
1771
        libs[remote] = 0
1✔
1772

1773
        if directory is None:
1!
1774
            directory = self.host
1✔
1775

1776
        directory = os.path.realpath(directory)
1✔
1777

1778
        res = {}
1✔
1779

1780
        seen = set()
1✔
1781

1782
        for lib, addr in libs.items():
1✔
1783
            local = os.path.realpath(os.path.join(directory, '.' + os.path.sep + lib))
1✔
1784
            if not local.startswith(directory):
1!
1785
                self.warning('This seems fishy: %r' % lib)
×
1786
                continue
×
1787

1788
            misc.mkdir_p(os.path.dirname(local))
1✔
1789

1790
            if lib not in seen:
1!
1791
                self.download_file(lib, local)
1✔
1792
                seen.add(lib)
1✔
1793
            res[local] = addr
1✔
1794

1795
        return res
1✔
1796

1797
    def interactive(self, shell=None):
1✔
1798
        """Create an interactive session.
1799

1800
        This is a simple wrapper for creating a new
1801
        :class:`pwnlib.tubes.ssh.ssh_channel` object and calling
1802
        :meth:`pwnlib.tubes.ssh.ssh_channel.interactive` on it."""
1803

1804
        s = self.shell(shell)
×
1805

1806
        if self.cwd != '.':
×
1807
            cmd = 'cd ' + sh_string(self.cwd)
×
1808
            s.sendline(packing._need_bytes(cmd, 2, 0x80))
×
1809

1810
        s.interactive()
×
1811
        s.close()
×
1812

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

1818
        Note:
1819
            This uses ``mktemp -d`` under the covers, sets permissions
1820
            on the directory to ``0700``.  This means that setuid binaries
1821
            will **not** be able to access files created in this directory.
1822

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

1825
        Arguments:
1826
            wd(string): Working directory.  Default is to auto-generate a directory
1827
                based on the result of running 'mktemp -d' on the remote machine.
1828
            symlink(bool,str): Create symlinks in the new directory.
1829

1830
                The default value, ``False``, implies that no symlinks should be
1831
                created.
1832

1833
                A string value is treated as a path that should be symlinked.
1834
                It is passed directly to the shell on the remote end for expansion,
1835
                so wildcards work.
1836

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

1840
        Examples:
1841
            >>> s =  ssh(host='example.pwnme')
1842
            >>> cwd = s.set_working_directory()
1843
            >>> s.ls()
1844
            b''
1845
            >>> packing._decode(s.pwd()) == cwd
1846
            True
1847

1848
            >>> s =  ssh(host='example.pwnme')
1849
            >>> homedir = s.pwd()
1850
            >>> _=s.touch('foo')
1851

1852
            >>> _=s.set_working_directory()
1853
            >>> assert s.ls() == b''
1854

1855
            >>> _=s.set_working_directory(homedir)
1856
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1857

1858
            >>> _=s.set_working_directory(symlink=True)
1859
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1860
            >>> assert homedir != s.pwd()
1861

1862
            >>> symlink=os.path.join(homedir,b'*')
1863
            >>> _=s.set_working_directory(symlink=symlink)
1864
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1865
            >>> assert homedir != s.pwd()
1866

1867
            >>> _=s.set_working_directory()
1868
            >>> io = s.system('pwd')
1869
            >>> io.recvallS().strip() == io.cwd
1870
            True
1871
            >>> io.cwd == s.cwd
1872
            True
1873
        """
1874
        status = 0
1✔
1875

1876
        if symlink and not isinstance(symlink, (six.binary_type, six.text_type)):
1✔
1877
            symlink = os.path.join(self.pwd(), b'*')
1✔
1878
        if not hasattr(symlink, 'encode') and hasattr(symlink, 'decode'):
1✔
1879
            symlink = symlink.decode('utf-8')
1✔
1880
            
1881
        if isinstance(wd, six.text_type):
1✔
1882
            wd = packing._need_bytes(wd, 2, 0x80)
1✔
1883

1884
        if not wd:
1✔
1885
            wd, status = self.run_to_end('x=$(mktemp -d) && cd $x && chmod +x . && echo $PWD', cwd='.')
1✔
1886
            wd = wd.strip()
1✔
1887

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

1891
        else:
1892
            cmd = b'ls ' + sh_string(wd)
1✔
1893
            _, status = self.run_to_end(cmd, wd = '.')
1✔
1894

1895
            if status:
1!
1896
                self.error("%r does not appear to exist" % wd)
×
1897

1898
        if not isinstance(wd, str):
1✔
1899
            wd = wd.decode('utf-8')
1✔
1900
        self.cwd = wd
1✔
1901

1902
        self.info("Working directory: %r" % self.cwd)
1✔
1903

1904
        if symlink:
1✔
1905
            self.ln('-s', symlink, '.')
1✔
1906

1907
        return wd
1✔
1908

1909
    def write(self, path, data):
1✔
1910
        """Wrapper around upload_data to match :func:`pwnlib.util.misc.write`"""
1911
        data = packing._need_bytes(data)
1✔
1912
        return self.upload_data(data, path)
1✔
1913

1914
    def read(self, path):
1✔
1915
        """Wrapper around download_data to match :func:`pwnlib.util.misc.read`"""
1916
        return self.download_data(path)
1✔
1917

1918
    def _init_remote_platform_info(self):
1✔
1919
        r"""Fills _platform_info, e.g.:
1920

1921
        ::
1922

1923
            {'distro': 'Ubuntu\n',
1924
             'distro_ver': '14.04\n',
1925
             'machine': 'x86_64',
1926
             'node': 'pwnable.kr',
1927
             'processor': 'x86_64',
1928
             'release': '3.11.0-12-generic',
1929
             'system': 'linux',
1930
             'version': '#19-ubuntu smp wed oct 9 16:20:46 utc 2013'}
1931
        """
1932
        if self._platform_info:
1✔
1933
            return
1✔
1934

1935
        def preexec():
1✔
1936
            import platform
×
1937
            print('\n'.join(platform.uname()))
×
1938

1939
        with context.quiet:
1✔
1940
            with self.process('true', preexec_fn=preexec) as io:
1✔
1941

1942
                self._platform_info = {
1✔
1943
                    'system': io.recvline().lower().strip().decode(),
1944
                    'node': io.recvline().lower().strip().decode(),
1945
                    'release': io.recvline().lower().strip().decode(),
1946
                    'version': io.recvline().lower().strip().decode(),
1947
                    'machine': io.recvline().lower().strip().decode(),
1948
                    'processor': io.recvline().lower().strip().decode(),
1949
                    'distro': 'Unknown',
1950
                    'distro_ver': ''
1951
                }
1952

1953
            try:
1✔
1954
                if not self.which('lsb_release'):
1!
1955
                    return
×
1956

1957
                with self.process(['lsb_release', '-irs']) as io:
1✔
1958
                    lsb_info = io.recvall().strip().decode()
1✔
1959
                    self._platform_info['distro'], self._platform_info['distro_ver'] = lsb_info.split()
1✔
1960
            except Exception:
×
1961
                pass
×
1962

1963
    @property
1✔
1964
    def os(self):
1✔
1965
        """:class:`str`: Operating System of the remote machine."""
1966
        try:
1✔
1967
            self._init_remote_platform_info()
1✔
1968
            with context.local(os=self._platform_info['system']):
1✔
1969
                return context.os
1✔
1970
        except Exception:
×
1971
            return "Unknown"
×
1972

1973

1974
    @property
1✔
1975
    def arch(self):
1✔
1976
        """:class:`str`: CPU Architecture of the remote machine."""
1977
        try:
1✔
1978
            self._init_remote_platform_info()
1✔
1979
            with context.local(arch=self._platform_info['machine']):
1✔
1980
                return context.arch
1✔
1981
        except Exception:
×
1982
            return "Unknown"
×
1983

1984
    @property
1✔
1985
    def bits(self):
1✔
1986
        """:class:`str`: Pointer size of the remote machine."""
1987
        try:
×
1988
            with context.local():
×
1989
                context.clear()
×
1990
                context.arch = self.arch
×
1991
                return context.bits
×
1992
        except Exception:
×
1993
            return context.bits
×
1994

1995
    @property
1✔
1996
    def version(self):
1✔
1997
        """:class:`tuple`: Kernel version of the remote machine."""
1998
        try:
1✔
1999
            self._init_remote_platform_info()
1✔
2000
            vers = self._platform_info['release']
1✔
2001

2002
            # 3.11.0-12-generic
2003
            expr = r'([0-9]+\.?)+'
1✔
2004

2005
            vers = re.search(expr, vers).group()
1✔
2006
            return tuple(map(int, vers.split('.')))
1✔
2007

2008
        except Exception:
×
2009
            return (0,0,0)
×
2010

2011
    @property
1✔
2012
    def distro(self):
1✔
2013
        """:class:`tuple`: Linux distribution name and release."""
2014
        try:
1✔
2015
            self._init_remote_platform_info()
1✔
2016
            return (self._platform_info['distro'], self._platform_info['distro_ver'])
1✔
2017
        except Exception:
×
2018
            return ("Unknown", "Unknown")
×
2019

2020
    @property
1✔
2021
    def aslr(self):
1✔
2022
        """:class:`bool`: Whether ASLR is enabled on the system.
2023

2024
        Example:
2025

2026
            >>> s = ssh("travis", "example.pwnme")
2027
            >>> s.aslr
2028
            True
2029
        """
2030
        if self._aslr is None:
1!
2031
            if self.os != 'linux':
1!
2032
                self.warn_once("Only Linux is supported for ASLR checks.")
×
2033
                self._aslr = False
×
2034

2035
            else:
2036
                with context.quiet:
1✔
2037
                    rvs = self.read('/proc/sys/kernel/randomize_va_space')
1✔
2038

2039
                self._aslr = not rvs.startswith(b'0')
1✔
2040

2041
        return self._aslr
1✔
2042

2043
    @property
1✔
2044
    def aslr_ulimit(self):
1✔
2045
        """:class:`bool`: Whether the entropy of 32-bit processes can be reduced with ulimit."""
2046
        import pwnlib.elf.elf
1✔
2047
        import pwnlib.shellcraft
1✔
2048

2049
        if self._aslr_ulimit is not None:
1!
2050
            return self._aslr_ulimit
×
2051

2052
        # This test must run a 32-bit binary, fix the architecture
2053
        arch = {
1✔
2054
            'amd64': 'i386',
2055
            'aarch64': 'arm'
2056
        }.get(self.arch, self.arch)
2057

2058
        with context.local(arch=arch, bits=32, os=self.os, aslr=True):
1!
2059
            with context.quiet:
1✔
2060
                try:
1✔
2061
                    sc = pwnlib.shellcraft.cat('/proc/self/maps') \
1✔
2062
                       + pwnlib.shellcraft.exit(0)
2063

2064
                    elf = pwnlib.elf.elf.ELF.from_assembly(sc, shared=True)
1✔
2065
                except Exception:
×
2066
                    self.warn_once("Can't determine ulimit ASLR status")
×
2067
                    self._aslr_ulimit = False
×
2068
                    return self._aslr_ulimit
×
2069

2070
                def preexec():
1✔
2071
                    import resource
×
2072
                    try:
×
2073
                        resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
2074
                    except Exception:
×
2075
                        pass
×
2076

2077
                # Move to a new temporary directory
2078
                cwd = self.cwd
1✔
2079
                tmp = self.set_working_directory()
1✔
2080

2081
                try:
1✔
2082
                    self.upload(elf.path, './aslr-test')
1✔
2083
                except IOError:
×
2084
                    self.warn_once("Couldn't check ASLR ulimit trick")
×
2085
                    self._aslr_ulimit = False
×
2086
                    return False
×
2087

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

2091
                # Move back to the old directory
2092
                self.cwd = cwd
1✔
2093

2094
                # Clean up the files
2095
                self.process(['rm', '-rf', tmp]).wait()
1✔
2096

2097
        # Check for 555555000 (1/3 of the address space for PAE)
2098
        # and for 40000000 (1/3 of the address space with 3BG barrier)
2099
        self._aslr_ulimit = bool(b'55555000' in maps or b'40000000' in maps)
1✔
2100

2101
        return self._aslr_ulimit
1✔
2102

2103
    def _checksec_cache(self, value=None):
1✔
2104
        path = self._get_cachefile('%s-%s' % (self.host, self.port))
1✔
2105

2106
        if value is not None:
1✔
2107
            with open(path, 'w+') as f:
1✔
2108
                f.write(value)
1✔
2109
        elif os.path.exists(path):
1✔
2110
            with open(path, 'r+') as f:
1✔
2111
                return f.read()
1✔
2112

2113
    def checksec(self, banner=True):
1✔
2114
        """checksec()
2115

2116
        Prints a helpful message about the remote system.
2117

2118
        Arguments:
2119
            banner(bool): Whether to print the path to the ELF binary.
2120
        """
2121
        cached = self._checksec_cache()
1✔
2122
        if cached:
1✔
2123
            return cached
1✔
2124

2125
        red    = text.red
1✔
2126
        green  = text.green
1✔
2127
        yellow = text.yellow
1✔
2128

2129
        res = [
1✔
2130
            "%s@%s:" % (self.user, self.host),
2131
            "Distro".ljust(10) + ' '.join(self.distro),
2132
            "OS:".ljust(10) + self.os,
2133
            "Arch:".ljust(10) + self.arch,
2134
            "Version:".ljust(10) + '.'.join(map(str, self.version)),
2135

2136
            "ASLR:".ljust(10) + {
2137
                True: green("Enabled"),
2138
                False: red("Disabled")
2139
            }[self.aslr]
2140
        ]
2141

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

2145
        cached = '\n'.join(res)
1✔
2146
        self._checksec_cache(cached)
1✔
2147
        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