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

Gallopsled / pwntools / 39eb5b9f9fc3ac2301be5a7a982096b3568d76e1

01 Nov 2023 10:20PM UTC coverage: 73.405% (+1.9%) from 71.525%
39eb5b9f9fc3ac2301be5a7a982096b3568d76e1

push

github-actions

Arusekk
Update CHANGELOG.md

3902 of 6416 branches covered (0.0%)

12255 of 16695 relevant lines covered (73.41%)

0.73 hits per line

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

68.45
/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)
×
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)
×
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):
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):
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):
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):
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):
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):
707
        return self._cwd
1✔
708

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

715
    @property
1✔
716
    def sftp(self):
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):
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 "$(which $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; which %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, wd = None, env = None, timeout = None, raw = True):
1✔
1145
        r"""system(process, tty = True, wd = 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', wd='/tmp')
1166
            >>> io.recvall()
1167
            b'/tmp\n'
1168
            >>> io.cwd
1169
            '/tmp'
1170
        """
1171

1172
        if wd is None:
1✔
1173
            wd = self.cwd
1✔
1174

1175
        if timeout is None:
1✔
1176
            timeout = self.timeout
1✔
1177

1178
        return ssh_channel(self, process, tty, wd, env, timeout = timeout, level = self.level, raw = raw)
1✔
1179

1180
    #: Backward compatibility.  Use :meth:`system`
1181
    run = system
1✔
1182

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

1187
        Note:
1188

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

1198
        with context.local(log_level='error'):
×
1199
            python = self.which('python')
×
1200

1201
            if not python:
×
1202
                self.error("Python is not installed on the remote system.")
×
1203

1204
            io = self.process(['','-c', script.strip()], executable=python, **kwargs)
×
1205
            result = io.recvall()
×
1206

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

1212

1213

1214
    def run_to_end(self, process, tty = False, wd = None, env = None):
1✔
1215
        r"""run_to_end(process, tty = False, timeout = Timeout.default, env = None) -> str
1216

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

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

1227
        with context.local(log_level = 'ERROR'):
1✔
1228
            c = self.run(process, tty, wd = wd, timeout = Timeout.default)
1✔
1229
            data = c.recvall()
1✔
1230
            retcode = c.wait()
1✔
1231
            c.close()
1✔
1232
            return data, retcode
1✔
1233

1234
    def connect_remote(self, host, port, timeout = Timeout.default):
1✔
1235
        r"""connect_remote(host, port, timeout = Timeout.default) -> ssh_connecter
1236

1237
        Connects to a host through an SSH connection. This is equivalent to
1238
        using the ``-L`` flag on ``ssh``.
1239

1240
        Returns a :class:`pwnlib.tubes.ssh.ssh_connecter` object.
1241

1242
        Examples:
1243
            >>> from pwn import *
1244
            >>> l = listen()
1245
            >>> s =  ssh(host='example.pwnme')
1246
            >>> a = s.connect_remote(s.host, l.lport)
1247
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1248
            >>> a.sendline(b'Hello')
1249
            >>> print(repr(b.recvline()))
1250
            b'Hello\n'
1251
        """
1252

1253
        return ssh_connecter(self, host, port, timeout, level=self.level)
1✔
1254

1255
    remote = connect_remote
1✔
1256

1257
    def listen_remote(self, port = 0, bind_address = '', timeout = Timeout.default):
1✔
1258
        r"""listen_remote(port = 0, bind_address = '', timeout = Timeout.default) -> ssh_connecter
1259

1260
        Listens remotely through an SSH connection. This is equivalent to
1261
        using the ``-R`` flag on ``ssh``.
1262

1263
        Returns a :class:`pwnlib.tubes.ssh.ssh_listener` object.
1264

1265
        Examples:
1266

1267
            >>> from pwn import *
1268
            >>> s =  ssh(host='example.pwnme')
1269
            >>> l = s.listen_remote()
1270
            >>> a = remote(s.host, l.port)
1271
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1272
            >>> a.sendline(b'Hello')
1273
            >>> print(repr(b.recvline()))
1274
            b'Hello\n'
1275
        """
1276

1277
        return ssh_listener(self, bind_address, port, timeout, level=self.level)
1✔
1278

1279
    listen = listen_remote
1✔
1280

1281
    def __getitem__(self, attr):
1✔
1282
        """Permits indexed access to run commands over SSH
1283

1284
        Examples:
1285

1286
            >>> s =  ssh(host='example.pwnme')
1287
            >>> print(repr(s['echo hello']))
1288
            b'hello'
1289
        """
1290
        return self.run(attr).recvall().strip()
1✔
1291

1292
    def __call__(self, attr):
1✔
1293
        """Permits function-style access to run commands over SSH
1294

1295
        Examples:
1296

1297
            >>> s =  ssh(host='example.pwnme')
1298
            >>> print(repr(s('echo hello')))
1299
            b'hello'
1300
        """
1301
        return self.run(attr).recvall().strip()
1✔
1302

1303
    def __getattr__(self, attr):
1✔
1304
        """Permits member access to run commands over SSH
1305

1306
        Examples:
1307

1308
            >>> s =  ssh(host='example.pwnme')
1309
            >>> s.echo('hello')
1310
            b'hello'
1311
            >>> s.whoami()
1312
            b'travis'
1313
            >>> s.echo(['huh','yay','args'])
1314
            b'huh yay args'
1315
        """
1316
        bad_attrs = [
1✔
1317
            'trait_names',          # ipython tab-complete
1318
        ]
1319

1320
        if attr in self.__dict__ \
1!
1321
        or attr in bad_attrs \
1322
        or attr.startswith('_'):
1323
            raise AttributeError
×
1324

1325
        @LocalContext
1✔
1326
        def runner(*args):
1327
            if len(args) == 1 and isinstance(args[0], (list, tuple)):
1✔
1328
                command = [attr]
1✔
1329
                command.extend(args[0])
1✔
1330
            else:
1331
                command = [attr]
1✔
1332
                command.extend(args)
1✔
1333
                command = b' '.join(packing._need_bytes(arg, min_wrong=0x80) for arg in command)
1✔
1334

1335
            return self.run(command).recvall().strip()
1✔
1336
        return runner
1✔
1337

1338
    def connected(self):
1✔
1339
        """Returns True if we are connected.
1340

1341
        Example:
1342

1343
            >>> s =  ssh(host='example.pwnme')
1344
            >>> s.connected()
1345
            True
1346
            >>> s.close()
1347
            >>> s.connected()
1348
            False
1349
        """
1350
        return bool(self.client and self.client.get_transport().is_active())
1✔
1351

1352
    def close(self):
1✔
1353
        """Close the connection."""
1354
        if self.client:
1!
1355
            self.client.close()
1✔
1356
            self.client = None
1✔
1357
            self.info("Closed connection to %r" % self.host)
1✔
1358

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

1377
        return misc.parse_ldd_output(packing._decode(data))
1✔
1378

1379
    def _get_fingerprint(self, remote):
1✔
1380
        cmd = '(sha256 || sha256sum || openssl sha256) 2>/dev/null < '
1✔
1381
        cmd = cmd + sh_string(remote)
1✔
1382

1383
        data, status = self.run_to_end(cmd)
1✔
1384

1385
        if status != 0:
1!
1386
            return None
×
1387

1388
        if not isinstance(data, str):
1!
1389
            data = data.decode('ascii')
×
1390

1391
        data = re.search("([a-fA-F0-9]{64})",data).group()
1✔
1392
        return data
1✔
1393

1394
    def _get_cachefile(self, fingerprint):
1✔
1395
        return os.path.join(self._cachedir, fingerprint)
1✔
1396

1397
    def _verify_local_fingerprint(self, fingerprint):
1✔
1398
        if not set(fingerprint).issubset(string.hexdigits) or \
1!
1399
           len(fingerprint) != 64:
1400
            self.error('Invalid fingerprint %r' % fingerprint)
×
1401
            return False
×
1402

1403
        local = self._get_cachefile(fingerprint)
1✔
1404
        if not os.path.isfile(local):
1✔
1405
            return False
1✔
1406

1407
        if hashes.sha256filehex(local) == fingerprint:
1!
1408
            return True
1✔
1409
        else:
1410
            os.unlink(local)
×
1411
            return False
×
1412

1413
    def _download_raw(self, remote, local, h):
1✔
1414
        def update(has, total):
1✔
1415
            h.status("%s/%s" % (misc.size(has), misc.size(total)))
1✔
1416

1417
        if self.sftp:
1✔
1418
            try:
1✔
1419
                self.sftp.get(remote, local, update)
1✔
1420
                return
1✔
1421
            except IOError:
×
1422
                pass
×
1423

1424
        cmd = 'wc -c < ' + sh_string(remote)
1✔
1425
        total, exitcode = self.run_to_end(cmd)
1✔
1426

1427
        if exitcode != 0:
1!
1428
            h.failure("%r does not exist or is not accessible" % remote)
×
1429
            return
×
1430

1431
        total = int(total)
1✔
1432

1433
        with context.local(log_level = 'ERROR'):
1✔
1434
            cmd = 'cat < ' + sh_string(remote)
1✔
1435
            c = self.run(cmd)
1✔
1436
        data = b''
1✔
1437

1438
        while True:
1✔
1439
            try:
1✔
1440
                data += c.recv()
1✔
1441
            except EOFError:
1✔
1442
                break
1✔
1443
            update(len(data), total)
1✔
1444

1445
        result = c.wait()
1✔
1446
        if result != 0:
1!
1447
            h.failure('Could not download file %r (%r)' % (remote, result))
×
1448
            return
×
1449

1450
        with open(local, 'wb') as fd:
1✔
1451
            fd.write(data)
1✔
1452

1453
    def _download_to_cache(self, remote, p):
1✔
1454

1455
        with context.local(log_level='error'):
1✔
1456
            remote = self.readlink('-f',remote)
1✔
1457
        if not hasattr(remote, 'encode'):
1!
1458
            remote = remote.decode('utf-8')
×
1459

1460
        fingerprint = self._get_fingerprint(remote)
1✔
1461
        if fingerprint is None:
1!
1462
            local = os.path.normpath(remote)
×
1463
            local = os.path.basename(local)
×
1464
            local += time.strftime('-%Y-%m-%d-%H:%M:%S')
×
1465
            local = os.path.join(self._cachedir, local)
×
1466

1467
            self._download_raw(remote, local, p)
×
1468
            return local
×
1469

1470
        local = self._get_cachefile(fingerprint)
1✔
1471

1472
        if self.cache and self._verify_local_fingerprint(fingerprint):
1✔
1473
            p.success('Found %r in ssh cache' % remote)
1✔
1474
        else:
1475
            self._download_raw(remote, local, p)
1✔
1476

1477
            if not self._verify_local_fingerprint(fingerprint):
1!
1478
                self.error('Could not download file %r', remote)
×
1479

1480
        return local
1✔
1481

1482
    def download_data(self, remote):
1✔
1483
        """Downloads a file from the remote server and returns it as a string.
1484

1485
        Arguments:
1486
            remote(str): The remote filename to download.
1487

1488

1489
        Examples:
1490
            >>> with open('/tmp/bar','w+') as f:
1491
            ...     _ = f.write('Hello, world')
1492
            >>> s =  ssh(host='example.pwnme',
1493
            ...         cache=False)
1494
            >>> s.download_data('/tmp/bar')
1495
            b'Hello, world'
1496
            >>> s._sftp = None
1497
            >>> s._tried_sftp = True
1498
            >>> s.download_data('/tmp/bar')
1499
            b'Hello, world'
1500

1501
        """
1502
        with self.progress('Downloading %r' % remote) as p:
1✔
1503
            with open(self._download_to_cache(remote, p), 'rb') as fd:
1✔
1504
                return fd.read()
1✔
1505

1506
    def download_file(self, remote, local = None):
1✔
1507
        """Downloads a file from the remote server.
1508

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

1512
        Arguments:
1513
            remote(str/bytes): The remote filename to download
1514
            local(str): The local filename to save it to. Default is to infer it from the remote filename.
1515
        
1516
        Examples:
1517
            >>> with open('/tmp/foobar','w+') as f:
1518
            ...     _ = f.write('Hello, world')
1519
            >>> s =  ssh(host='example.pwnme',
1520
            ...         cache=False)
1521
            >>> _ = s.set_working_directory(wd='/tmp')
1522
            >>> _ = s.download_file('foobar', 'barfoo')
1523
            >>> with open('barfoo','r') as f:
1524
            ...     print(f.read())
1525
            Hello, world
1526
        """
1527

1528

1529
        if not local:
1✔
1530
            local = os.path.basename(os.path.normpath(remote))
1✔
1531

1532
        with self.progress('Downloading %r to %r' % (remote, local)) as p:
1✔
1533
            local_tmp = self._download_to_cache(remote, p)
1✔
1534

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

1539
    def download_dir(self, remote=None, local=None, ignore_failed_read=False):
1✔
1540
        """Recursively downloads a directory from the remote server
1541

1542
        Arguments:
1543
            local: Local directory
1544
            remote: Remote directory
1545
        """
1546
        remote = packing._encode(remote or self.cwd)
×
1547

1548
        if self.sftp:
×
1549
            remote = packing._encode(self.sftp.normalize(remote))
×
1550
        else:
1551
            with context.local(log_level='error'):
×
1552
                remote = self.system(b'readlink -f ' + sh_string(remote)).recvall().strip()
×
1553

1554
        local = local or '.'
×
1555
        local = os.path.expanduser(local)
×
1556

1557
        self.info("Downloading %r to %r" % (remote, local))
×
1558

1559
        if ignore_failed_read:
×
1560
            opts = b" --ignore-failed-read"
×
1561
        else:
1562
            opts = b""
×
1563
        with context.local(log_level='error'):
×
1564
            remote_tar = self.mktemp()
×
1565
            cmd = b'tar %s -C %s -czf %s .' % \
×
1566
                  (opts,
1567
                   sh_string(remote),
1568
                   sh_string(remote_tar))
1569
            tar = self.system(cmd)
×
1570

1571
            if 0 != tar.wait():
×
1572
                self.error("Could not create remote tar")
×
1573

1574
            local_tar = tempfile.NamedTemporaryFile(suffix='.tar.gz')
×
1575
            self.download_file(remote_tar, local_tar.name)
×
1576

1577
            # Delete temporary tarfile from remote host
1578
            if self.sftp:
×
1579
                self.unlink(remote_tar)
×
1580
            else:
1581
                self.system(b'rm ' + sh_string(remote_tar)).wait()
×
1582
            tar = tarfile.open(local_tar.name)
×
1583
            tar.extractall(local)
×
1584

1585

1586
    def upload_data(self, data, remote):
1✔
1587
        """Uploads some data into a file on the remote server.
1588

1589
        Arguments:
1590
            data(str): The data to upload.
1591
            remote(str): The filename to upload it to.
1592

1593
        Example:
1594
            >>> s =  ssh(host='example.pwnme')
1595
            >>> s.upload_data(b'Hello, world', '/tmp/upload_foo')
1596
            >>> print(open('/tmp/upload_foo').read())
1597
            Hello, world
1598
            >>> s._sftp = False
1599
            >>> s._tried_sftp = True
1600
            >>> s.upload_data(b'Hello, world', '/tmp/upload_bar')
1601
            >>> print(open('/tmp/upload_bar').read())
1602
            Hello, world
1603
        """
1604
        data = packing._need_bytes(data)
1✔
1605
        # If a relative path was provided, prepend the cwd
1606
        if os.path.normpath(remote) == os.path.basename(remote):
1✔
1607
            remote = os.path.join(self.cwd, remote)
1✔
1608

1609
        if self.sftp:
1✔
1610
            with tempfile.NamedTemporaryFile() as f:
1✔
1611
                f.write(data)
1✔
1612
                f.flush()
1✔
1613
                self.sftp.put(f.name, remote)
1✔
1614
                return
1✔
1615

1616
        with context.local(log_level = 'ERROR'):
1✔
1617
            cmd = 'cat > ' + sh_string(remote)
1✔
1618
            s = self.run(cmd, tty=False)
1✔
1619
            s.send(data)
1✔
1620
            s.shutdown('send')
1✔
1621
            data   = s.recvall()
1✔
1622
            result = s.wait()
1✔
1623
            if result != 0:
1!
1624
                self.error("Could not upload file %r (%r)\n%s" % (remote, result, data))
×
1625

1626
    def upload_file(self, filename, remote = None):
1✔
1627
        """Uploads a file to the remote server. Returns the remote filename.
1628

1629
        Arguments:
1630
        filename(str): The local filename to download
1631
        remote(str): The remote filename to save it to. Default is to infer it from the local filename."""
1632

1633

1634
        if remote is None:
1✔
1635
            remote = os.path.normpath(filename)
1✔
1636
            remote = os.path.basename(remote)
1✔
1637
            remote = os.path.join(self.cwd, remote)
1✔
1638

1639
        with open(filename, 'rb') as fd:
1✔
1640
            data = fd.read()
1✔
1641

1642
        self.info("Uploading %r to %r" % (filename,remote))
1✔
1643
        self.upload_data(data, remote)
1✔
1644

1645
        return remote
1✔
1646

1647
    def upload_dir(self, local, remote=None):
1✔
1648
        """Recursively uploads a directory onto the remote server
1649

1650
        Arguments:
1651
            local: Local directory
1652
            remote: Remote directory
1653
        """
1654

1655
        remote    = packing._encode(remote or self.cwd)
×
1656

1657
        local     = os.path.expanduser(local)
×
1658
        dirname   = os.path.dirname(local)
×
1659
        basename  = os.path.basename(local)
×
1660

1661
        if not os.path.isdir(local):
×
1662
            self.error("%r is not a directory" % local)
×
1663

1664
        msg = "Uploading %r to %r" % (basename,remote)
×
1665
        with self.waitfor(msg):
×
1666
            # Generate a tarfile with everything inside of it
1667
            local_tar  = tempfile.mktemp()
×
1668
            with tarfile.open(local_tar, 'w:gz') as tar:
×
1669
                tar.add(local, basename)
×
1670

1671
            # Upload and extract it
1672
            with context.local(log_level='error'):
×
1673
                remote_tar = self.mktemp('--suffix=.tar.gz')
×
1674
                self.upload_file(local_tar, remote_tar)
×
1675

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

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

1682
    def upload(self, file_or_directory, remote=None):
1✔
1683
        """upload(file_or_directory, remote=None)
1684

1685
        Upload a file or directory to the remote host.
1686

1687
        Arguments:
1688
            file_or_directory(str): Path to the file or directory to download.
1689
            remote(str): Local path to store the data.
1690
                By default, uses the working directory.
1691
        """
1692
        if isinstance(file_or_directory, str):
1!
1693
            file_or_directory = os.path.expanduser(file_or_directory)
1✔
1694
            file_or_directory = os.path.expandvars(file_or_directory)
1✔
1695

1696
        if os.path.isfile(file_or_directory):
1!
1697
            return self.upload_file(file_or_directory, remote)
1✔
1698

1699
        if os.path.isdir(file_or_directory):
×
1700
            return self.upload_dir(file_or_directory, remote)
×
1701

1702
        self.error('%r does not exist' % file_or_directory)
×
1703

1704
    def download(self, file_or_directory, local=None):
1✔
1705
        """download(file_or_directory, local=None)
1706

1707
        Download a file or directory from the remote host.
1708

1709
        Arguments:
1710
            file_or_directory(str): Path to the file or directory to download.
1711
            local(str): Local path to store the data.
1712
                By default, uses the current directory.
1713
        
1714

1715
        Examples:
1716
            >>> with open('/tmp/foobar','w+') as f:
1717
            ...     _ = f.write('Hello, world')
1718
            >>> s =  ssh(host='example.pwnme',
1719
            ...         cache=False)
1720
            >>> _ = s.set_working_directory('/tmp')
1721
            >>> _ = s.download('foobar', 'barfoo')
1722
            >>> with open('barfoo','r') as f:
1723
            ...     print(f.read())
1724
            Hello, world
1725
        """
1726
        file_or_directory = packing._encode(file_or_directory)
1✔
1727
        with self.system(b'test -d ' + sh_string(file_or_directory)) as io:
1✔
1728
            is_dir = io.wait()
1✔
1729

1730
        if 0 == is_dir:
1!
1731
            self.download_dir(file_or_directory, local)
×
1732
        else:
1733
            self.download_file(file_or_directory, local)
1✔
1734

1735
    put = upload
1✔
1736
    get = download
1✔
1737

1738
    def unlink(self, file):
1✔
1739
        """unlink(file)
1740

1741
        Delete the file on the remote host
1742

1743
        Arguments:
1744
            file(str): Path to the file
1745
        """
1746
        if not self.sftp:
×
1747
            self.error("unlink() is only supported if SFTP is supported")
×
1748

1749
        return self.sftp.unlink(file)
×
1750

1751
    def libs(self, remote, directory = None):
1✔
1752
        """Downloads the libraries referred to by a file.
1753

1754
        This is done by running ldd on the remote server, parsing the output
1755
        and downloading the relevant files.
1756

1757
        The directory argument specified where to download the files. This defaults
1758
        to './$HOSTNAME' where $HOSTNAME is the hostname of the remote server."""
1759

1760
        libs = self._libs_remote(remote)
1✔
1761

1762
        remote = packing._decode(self.readlink('-f',remote).strip())
1✔
1763
        libs[remote] = 0
1✔
1764

1765
        if directory is None:
1!
1766
            directory = self.host
1✔
1767

1768
        directory = os.path.realpath(directory)
1✔
1769

1770
        res = {}
1✔
1771

1772
        seen = set()
1✔
1773

1774
        for lib, addr in libs.items():
1✔
1775
            local = os.path.realpath(os.path.join(directory, '.' + os.path.sep + lib))
1✔
1776
            if not local.startswith(directory):
1!
1777
                self.warning('This seems fishy: %r' % lib)
×
1778
                continue
×
1779

1780
            misc.mkdir_p(os.path.dirname(local))
1✔
1781

1782
            if lib not in seen:
1!
1783
                self.download_file(lib, local)
1✔
1784
                seen.add(lib)
1✔
1785
            res[local] = addr
1✔
1786

1787
        return res
1✔
1788

1789
    def interactive(self, shell=None):
1✔
1790
        """Create an interactive session.
1791

1792
        This is a simple wrapper for creating a new
1793
        :class:`pwnlib.tubes.ssh.ssh_channel` object and calling
1794
        :meth:`pwnlib.tubes.ssh.ssh_channel.interactive` on it."""
1795

1796
        s = self.shell(shell)
×
1797

1798
        if self.cwd != '.':
×
1799
            cmd = 'cd ' + sh_string(self.cwd)
×
1800
            s.sendline(packing._need_bytes(cmd, 2, 0x80))
×
1801

1802
        s.interactive()
×
1803
        s.close()
×
1804

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

1810
        Note:
1811
            This uses ``mktemp -d`` under the covers, sets permissions
1812
            on the directory to ``0700``.  This means that setuid binaries
1813
            will **not** be able to access files created in this directory.
1814

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

1817
        Arguments:
1818
            wd(string): Working directory.  Default is to auto-generate a directory
1819
                based on the result of running 'mktemp -d' on the remote machine.
1820
            symlink(bool,str): Create symlinks in the new directory.
1821

1822
                The default value, ``False``, implies that no symlinks should be
1823
                created.
1824

1825
                A string value is treated as a path that should be symlinked.
1826
                It is passed directly to the shell on the remote end for expansion,
1827
                so wildcards work.
1828

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

1832
        Examples:
1833
            >>> s =  ssh(host='example.pwnme')
1834
            >>> cwd = s.set_working_directory()
1835
            >>> s.ls()
1836
            b''
1837
            >>> packing._decode(s.pwd()) == cwd
1838
            True
1839

1840
            >>> s =  ssh(host='example.pwnme')
1841
            >>> homedir = s.pwd()
1842
            >>> _=s.touch('foo')
1843

1844
            >>> _=s.set_working_directory()
1845
            >>> assert s.ls() == b''
1846

1847
            >>> _=s.set_working_directory(homedir)
1848
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1849

1850
            >>> _=s.set_working_directory(symlink=True)
1851
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1852
            >>> assert homedir != s.pwd()
1853

1854
            >>> symlink=os.path.join(homedir,b'*')
1855
            >>> _=s.set_working_directory(symlink=symlink)
1856
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1857
            >>> assert homedir != s.pwd()
1858

1859
            >>> _=s.set_working_directory()
1860
            >>> io = s.system('pwd')
1861
            >>> io.recvallS().strip() == io.cwd
1862
            True
1863
            >>> io.cwd == s.cwd
1864
            True
1865
        """
1866
        status = 0
1✔
1867

1868
        if symlink and not isinstance(symlink, (six.binary_type, six.text_type)):
1✔
1869
            symlink = os.path.join(self.pwd(), b'*')
1✔
1870
        if not hasattr(symlink, 'encode') and hasattr(symlink, 'decode'):
1!
1871
            symlink = symlink.decode('utf-8')
×
1872
            
1873
        if isinstance(wd, six.text_type):
1!
1874
            wd = packing._need_bytes(wd, 2, 0x80)
×
1875

1876
        if not wd:
1✔
1877
            wd, status = self.run_to_end('x=$(mktemp -d) && cd $x && chmod +x . && echo $PWD', wd='.')
1✔
1878
            wd = wd.strip()
1✔
1879

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

1883
        else:
1884
            cmd = b'ls ' + sh_string(wd)
1✔
1885
            _, status = self.run_to_end(cmd, wd = '.')
1✔
1886

1887
            if status:
1!
1888
                self.error("%r does not appear to exist" % wd)
×
1889

1890
        if not isinstance(wd, str):
1!
1891
            wd = wd.decode('utf-8')
×
1892
        self.cwd = wd
1✔
1893

1894
        self.info("Working directory: %r" % self.cwd)
1✔
1895

1896
        if symlink:
1✔
1897
            self.ln('-s', symlink, '.')
1✔
1898

1899
        return wd
1✔
1900

1901
    def write(self, path, data):
1✔
1902
        """Wrapper around upload_data to match :func:`pwnlib.util.misc.write`"""
1903
        data = packing._need_bytes(data)
1✔
1904
        return self.upload_data(data, path)
1✔
1905

1906
    def read(self, path):
1✔
1907
        """Wrapper around download_data to match :func:`pwnlib.util.misc.read`"""
1908
        return self.download_data(path)
1✔
1909

1910
    def _init_remote_platform_info(self):
1✔
1911
        r"""Fills _platform_info, e.g.:
1912

1913
        ::
1914

1915
            {'distro': 'Ubuntu\n',
1916
             'distro_ver': '14.04\n',
1917
             'machine': 'x86_64',
1918
             'node': 'pwnable.kr',
1919
             'processor': 'x86_64',
1920
             'release': '3.11.0-12-generic',
1921
             'system': 'linux',
1922
             'version': '#19-ubuntu smp wed oct 9 16:20:46 utc 2013'}
1923
        """
1924
        if self._platform_info:
1✔
1925
            return
1✔
1926

1927
        def preexec():
1✔
1928
            import platform
×
1929
            print('\n'.join(platform.uname()))
×
1930

1931
        with context.quiet:
1✔
1932
            with self.process('true', preexec_fn=preexec) as io:
1✔
1933

1934
                self._platform_info = {
1✔
1935
                    'system': io.recvline().lower().strip().decode(),
1936
                    'node': io.recvline().lower().strip().decode(),
1937
                    'release': io.recvline().lower().strip().decode(),
1938
                    'version': io.recvline().lower().strip().decode(),
1939
                    'machine': io.recvline().lower().strip().decode(),
1940
                    'processor': io.recvline().lower().strip().decode(),
1941
                    'distro': 'Unknown',
1942
                    'distro_ver': ''
1943
                }
1944

1945
            try:
1✔
1946
                if not self.which('lsb_release'):
1!
1947
                    return
×
1948

1949
                with self.process(['lsb_release', '-irs']) as io:
1✔
1950
                    lsb_info = io.recvall().strip().decode()
1✔
1951
                    self._platform_info['distro'], self._platform_info['distro_ver'] = lsb_info.split()
1✔
1952
            except Exception:
×
1953
                pass
×
1954

1955
    @property
1✔
1956
    def os(self):
1957
        """:class:`str`: Operating System of the remote machine."""
1958
        try:
1✔
1959
            self._init_remote_platform_info()
1✔
1960
            with context.local(os=self._platform_info['system']):
1✔
1961
                return context.os
1✔
1962
        except Exception:
×
1963
            return "Unknown"
×
1964

1965

1966
    @property
1✔
1967
    def arch(self):
1968
        """:class:`str`: CPU Architecture of the remote machine."""
1969
        try:
1✔
1970
            self._init_remote_platform_info()
1✔
1971
            with context.local(arch=self._platform_info['machine']):
1✔
1972
                return context.arch
1✔
1973
        except Exception:
×
1974
            return "Unknown"
×
1975

1976
    @property
1✔
1977
    def bits(self):
1978
        """:class:`str`: Pointer size of the remote machine."""
1979
        try:
×
1980
            with context.local():
×
1981
                context.clear()
×
1982
                context.arch = self.arch
×
1983
                return context.bits
×
1984
        except Exception:
×
1985
            return context.bits
×
1986

1987
    @property
1✔
1988
    def version(self):
1989
        """:class:`tuple`: Kernel version of the remote machine."""
1990
        try:
1✔
1991
            self._init_remote_platform_info()
1✔
1992
            vers = self._platform_info['release']
1✔
1993

1994
            # 3.11.0-12-generic
1995
            expr = r'([0-9]+\.?)+'
1✔
1996

1997
            vers = re.search(expr, vers).group()
1✔
1998
            return tuple(map(int, vers.split('.')))
1✔
1999

2000
        except Exception:
×
2001
            return (0,0,0)
×
2002

2003
    @property
1✔
2004
    def distro(self):
2005
        """:class:`tuple`: Linux distribution name and release."""
2006
        try:
1✔
2007
            self._init_remote_platform_info()
1✔
2008
            return (self._platform_info['distro'], self._platform_info['distro_ver'])
1✔
2009
        except Exception:
×
2010
            return ("Unknown", "Unknown")
×
2011

2012
    @property
1✔
2013
    def aslr(self):
2014
        """:class:`bool`: Whether ASLR is enabled on the system.
2015

2016
        Example:
2017

2018
            >>> s = ssh("travis", "example.pwnme")
2019
            >>> s.aslr
2020
            True
2021
        """
2022
        if self._aslr is None:
1!
2023
            if self.os != 'linux':
1!
2024
                self.warn_once("Only Linux is supported for ASLR checks.")
×
2025
                self._aslr = False
×
2026

2027
            else:
2028
                with context.quiet:
1✔
2029
                    rvs = self.read('/proc/sys/kernel/randomize_va_space')
1✔
2030

2031
                self._aslr = not rvs.startswith(b'0')
1✔
2032

2033
        return self._aslr
1✔
2034

2035
    @property
1✔
2036
    def aslr_ulimit(self):
2037
        """:class:`bool`: Whether the entropy of 32-bit processes can be reduced with ulimit."""
2038
        import pwnlib.elf.elf
1✔
2039
        import pwnlib.shellcraft
1✔
2040

2041
        if self._aslr_ulimit is not None:
1!
2042
            return self._aslr_ulimit
×
2043

2044
        # This test must run a 32-bit binary, fix the architecture
2045
        arch = {
1✔
2046
            'amd64': 'i386',
2047
            'aarch64': 'arm'
2048
        }.get(self.arch, self.arch)
2049

2050
        with context.local(arch=arch, bits=32, os=self.os, aslr=True):
1✔
2051
            with context.quiet:
1✔
2052
                try:
1✔
2053
                    sc = pwnlib.shellcraft.cat('/proc/self/maps') \
1✔
2054
                       + pwnlib.shellcraft.exit(0)
2055

2056
                    elf = pwnlib.elf.elf.ELF.from_assembly(sc, shared=True)
1✔
2057
                except Exception:
×
2058
                    self.warn_once("Can't determine ulimit ASLR status")
×
2059
                    self._aslr_ulimit = False
×
2060
                    return self._aslr_ulimit
×
2061

2062
                def preexec():
1✔
2063
                    import resource
×
2064
                    try:
×
2065
                        resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
2066
                    except Exception:
×
2067
                        pass
×
2068

2069
                # Move to a new temporary directory
2070
                cwd = self.cwd
1✔
2071
                tmp = self.set_working_directory()
1✔
2072

2073
                try:
1✔
2074
                    self.upload(elf.path, './aslr-test')
1✔
2075
                except IOError:
×
2076
                    self.warn_once("Couldn't check ASLR ulimit trick")
×
2077
                    self._aslr_ulimit = False
×
2078
                    return False
×
2079

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

2083
                # Move back to the old directory
2084
                self.cwd = cwd
1✔
2085

2086
                # Clean up the files
2087
                self.process(['rm', '-rf', tmp]).wait()
1✔
2088

2089
        # Check for 555555000 (1/3 of the address space for PAE)
2090
        # and for 40000000 (1/3 of the address space with 3BG barrier)
2091
        self._aslr_ulimit = bool(b'55555000' in maps or b'40000000' in maps)
1✔
2092

2093
        return self._aslr_ulimit
1✔
2094

2095
    def _checksec_cache(self, value=None):
1✔
2096
        path = self._get_cachefile('%s-%s' % (self.host, self.port))
1✔
2097

2098
        if value is not None:
1✔
2099
            with open(path, 'w+') as f:
1✔
2100
                f.write(value)
1✔
2101
        elif os.path.exists(path):
1✔
2102
            with open(path, 'r+') as f:
1✔
2103
                return f.read()
1✔
2104

2105
    def checksec(self, banner=True):
1✔
2106
        """checksec()
2107

2108
        Prints a helpful message about the remote system.
2109

2110
        Arguments:
2111
            banner(bool): Whether to print the path to the ELF binary.
2112
        """
2113
        cached = self._checksec_cache()
1✔
2114
        if cached:
1✔
2115
            return cached
1✔
2116

2117
        red    = text.red
1✔
2118
        green  = text.green
1✔
2119
        yellow = text.yellow
1✔
2120

2121
        res = [
1✔
2122
            "%s@%s:" % (self.user, self.host),
2123
            "Distro".ljust(10) + ' '.join(self.distro),
2124
            "OS:".ljust(10) + self.os,
2125
            "Arch:".ljust(10) + self.arch,
2126
            "Version:".ljust(10) + '.'.join(map(str, self.version)),
2127

2128
            "ASLR:".ljust(10) + {
2129
                True: green("Enabled"),
2130
                False: red("Disabled")
2131
            }[self.aslr]
2132
        ]
2133

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

2137
        cached = '\n'.join(res)
1✔
2138
        self._checksec_cache(cached)
1✔
2139
        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