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

Gallopsled / pwntools / e5407603f02b03e178c2db866355fda60796b22d

pending completion
e5407603f02b03e178c2db866355fda60796b22d

push

github-actions

GitHub
Added arch and bits specific flag to asm call from elf wrapper (#2137)

3867 of 6353 branches covered (60.87%)

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

12187 of 16570 relevant lines covered (73.55%)

0.74 hits per line

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

65.24
/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, wd = 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  = wd or '.'
1✔
72
        if isinstance(wd, six.text_type):
1✔
73
            wd = packing._need_bytes(wd, 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 wd:
1✔
84
            process = b'cd ' + sh_string(wd) + 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
            >>> s.process(['pwd'], cwd='/tmp').recvall()
845
            b'/tmp\n'
846
            >>> p = s.process(['python','-c','import os; os.write(1, os.read(2, 1024))'], stderr=0)
847
            >>> p.send(b'hello')
848
            >>> p.recv()
849
            b'hello'
850
            >>> s.process(['/bin/echo', 'hello']).recvall()
851
            b'hello\n'
852
            >>> s.process(['/bin/echo', 'hello'], stdout='/dev/null').recvall()
853
            b''
854
            >>> s.process(['/usr/bin/env'], env={}).recvall()
855
            b''
856
            >>> s.process('/usr/bin/env', env={'A':'B'}).recvall()
857
            b'A=B\n'
858

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

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

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

876
            >>> s.process('echo hello', shell=True).recvall()
877
            b'hello\n'
878

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

886
        aslr      = aslr if aslr is not None else context.aslr
1✔
887

888
        argv, env = misc.normalize_argv_env(argv, env, self)
1✔
889

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

895
        executable = executable or argv[0]
1✔
896
        cwd        = cwd or self.cwd
1✔
897

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

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

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

914
        if not isinstance(func, types.FunctionType):
1✔
915
            self.error("preexec_fn must be a function")
1✔
916

917
        func_name = func.__name__
1✔
918
        if func_name == (lambda: 0).__name__:
1!
919
            self.error("preexec_fn cannot be a lambda")
1✔
920

921
        func_src  = inspect.getsource(func).strip()
1✔
922
        setuid = True if setuid is None else bool(setuid)
1✔
923

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

936
os.chdir(%(cwd)r)
937

938
environ = getattr(os, 'environb', os.environ)
939

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

947
def is_exe(path):
948
    return os.path.isfile(path) and os.access(path, os.X_OK)
949

950
PATH = environ.get(b'PATH',b'').split(os.pathsep.encode())
951

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

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

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

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

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

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

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

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

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

1016
    resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
1017

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

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

1031
%(func_src)s
1032
%(func_name)s(*%(func_args)r)
1033

1034
os.execve(exe, argv, env)
1035
""" % locals()  # """
1036

1037
        script = script.strip()
1✔
1038

1039
        self.debug("Created execve script:\n" + script)
1✔
1040

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

1046
            self.info("Uploading execve script to %r" % tmpfile)
×
1047
            self.upload_data(script, tmpfile)
×
1048
            return tmpfile
×
1049

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

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

1064
        if timeout == Timeout.default:
1✔
1065
            timeout = self.timeout
1✔
1066

1067
        with self.progress(msg) as h:
1✔
1068

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

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

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

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

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

1107
            h.success('pid %i' % python.pid)
1✔
1108

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

1114
            if self.aslr_ulimit:
×
1115
                message += "Unlimited stack size should de-randomize shared libraries."
×
1116

1117
            self.warn_once(message)
×
1118

1119
        elif not aslr:
1!
1120
            self.warn_once("ASLR is disabled for %r!" % python.executable)
×
1121

1122
        return python
1✔
1123

1124
    def which(self, program):
1✔
1125
        """which(program) -> str
1126

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

1134
        result = self.run('export PATH=$PATH:$PWD; which %s' % program).recvall().strip().decode()
1✔
1135

1136
        if ('/%s' % program) not in result:
1✔
1137
            return None
1✔
1138

1139
        return result
1✔
1140

1141
    def system(self, process, tty = True, wd = None, env = None, timeout = None, raw = True):
1✔
1142
        r"""system(process, tty = True, wd = None, env = None, timeout = Timeout.default, raw = True) -> ssh_channel
1143

1144
        Open a new channel with a specific process inside. If `tty` is True,
1145
        then a TTY is requested on the remote server.
1146

1147
        If `raw` is True, terminal control codes are ignored and input is not
1148
        echoed back.
1149

1150
        Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
1151

1152
        Examples:
1153
            >>> s =  ssh(host='example.pwnme')
1154
            >>> py = s.run('python -i')
1155
            >>> _ = py.recvuntil(b'>>> ')
1156
            >>> py.sendline(b'print(2+2)')
1157
            >>> py.sendline(b'exit')
1158
            >>> print(repr(py.recvline()))
1159
            b'4\n'
1160
            >>> s.system('env | grep -a AAAA', env={'AAAA': b'\x90'}).recvall()
1161
            b'AAAA=\x90\n'
1162
        """
1163

1164
        if wd is None:
1✔
1165
            wd = self.cwd
1✔
1166

1167
        if timeout is None:
1✔
1168
            timeout = self.timeout
1✔
1169

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

1172
    #: Backward compatibility.  Use :meth:`system`
1173
    run = system
1✔
1174

1175
    def getenv(self, variable, **kwargs):
1✔
1176
        """Retrieve the address of an environment variable on the remote
1177
        system.
1178

1179
        Note:
1180

1181
            The exact address will differ based on what other environment
1182
            variables are set, as well as argv[0].  In order to ensure that
1183
            the path is *exactly* the same, it is recommended to invoke the
1184
            process with ``argv=[]``.
1185
        """
1186
        script = '''
×
1187
from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r))
1188
''' % variable
1189

1190
        with context.local(log_level='error'):
×
1191
            python = self.which('python')
×
1192

1193
            if not python:
×
1194
                self.error("Python is not installed on the remote system.")
×
1195

1196
            io = self.process(['','-c', script.strip()], executable=python, **kwargs)
×
1197
            result = io.recvall()
×
1198

1199
        try:
×
1200
            return int(result) & context.mask
×
1201
        except ValueError:
×
1202
            self.exception("Could not look up environment variable %r" % variable)
×
1203

1204

1205

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

1209
        Run a command on the remote server and return a tuple with
1210
        (data, exit_status). If `tty` is True, then the command is run inside
1211
        a TTY on the remote server.
1212

1213
        Examples:
1214
            >>> s =  ssh(host='example.pwnme')
1215
            >>> print(s.run_to_end('echo Hello; exit 17'))
1216
            (b'Hello\n', 17)
1217
            """
1218

1219
        with context.local(log_level = 'ERROR'):
1✔
1220
            c = self.run(process, tty, wd = wd, timeout = Timeout.default)
1✔
1221
            data = c.recvall()
1✔
1222
            retcode = c.wait()
1✔
1223
            c.close()
1✔
1224
            return data, retcode
1✔
1225

1226
    def connect_remote(self, host, port, timeout = Timeout.default):
1✔
1227
        r"""connect_remote(host, port, timeout = Timeout.default) -> ssh_connecter
1228

1229
        Connects to a host through an SSH connection. This is equivalent to
1230
        using the ``-L`` flag on ``ssh``.
1231

1232
        Returns a :class:`pwnlib.tubes.ssh.ssh_connecter` object.
1233

1234
        Examples:
1235
            >>> from pwn import *
1236
            >>> l = listen()
1237
            >>> s =  ssh(host='example.pwnme')
1238
            >>> a = s.connect_remote(s.host, l.lport)
1239
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1240
            >>> a.sendline(b'Hello')
1241
            >>> print(repr(b.recvline()))
1242
            b'Hello\n'
1243
        """
1244

1245
        return ssh_connecter(self, host, port, timeout, level=self.level)
1✔
1246

1247
    remote = connect_remote
1✔
1248

1249
    def listen_remote(self, port = 0, bind_address = '', timeout = Timeout.default):
1✔
1250
        r"""listen_remote(port = 0, bind_address = '', timeout = Timeout.default) -> ssh_connecter
1251

1252
        Listens remotely through an SSH connection. This is equivalent to
1253
        using the ``-R`` flag on ``ssh``.
1254

1255
        Returns a :class:`pwnlib.tubes.ssh.ssh_listener` object.
1256

1257
        Examples:
1258

1259
            >>> from pwn import *
1260
            >>> s =  ssh(host='example.pwnme')
1261
            >>> l = s.listen_remote()
1262
            >>> a = remote(s.host, l.port)
1263
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1264
            >>> a.sendline(b'Hello')
1265
            >>> print(repr(b.recvline()))
1266
            b'Hello\n'
1267
        """
1268

1269
        return ssh_listener(self, bind_address, port, timeout, level=self.level)
1✔
1270

1271
    listen = listen_remote
1✔
1272

1273
    def __getitem__(self, attr):
1✔
1274
        """Permits indexed access to run commands over SSH
1275

1276
        Examples:
1277

1278
            >>> s =  ssh(host='example.pwnme')
1279
            >>> print(repr(s['echo hello']))
1280
            b'hello'
1281
        """
1282
        return self.run(attr).recvall().strip()
1✔
1283

1284
    def __call__(self, attr):
1✔
1285
        """Permits function-style access to run commands over SSH
1286

1287
        Examples:
1288

1289
            >>> s =  ssh(host='example.pwnme')
1290
            >>> print(repr(s('echo hello')))
1291
            b'hello'
1292
        """
1293
        return self.run(attr).recvall().strip()
1✔
1294

1295
    def __getattr__(self, attr):
1✔
1296
        """Permits member access to run commands over SSH
1297

1298
        Examples:
1299

1300
            >>> s =  ssh(host='example.pwnme')
1301
            >>> s.echo('hello')
1302
            b'hello'
1303
            >>> s.whoami()
1304
            b'travis'
1305
            >>> s.echo(['huh','yay','args'])
1306
            b'huh yay args'
1307
        """
1308
        bad_attrs = [
1✔
1309
            'trait_names',          # ipython tab-complete
1310
        ]
1311

1312
        if attr in self.__dict__ \
1!
1313
        or attr in bad_attrs \
1314
        or attr.startswith('_'):
1315
            raise AttributeError
×
1316

1317
        @LocalContext
1✔
1318
        def runner(*args):
1319
            if len(args) == 1 and isinstance(args[0], (list, tuple)):
1✔
1320
                command = [attr]
1✔
1321
                command.extend(args[0])
1✔
1322
            else:
1323
                command = [attr]
1✔
1324
                command.extend(args)
1✔
1325
                command = b' '.join(packing._need_bytes(arg, min_wrong=0x80) for arg in command)
1✔
1326

1327
            return self.run(command).recvall().strip()
1✔
1328
        return runner
1✔
1329

1330
    def connected(self):
1✔
1331
        """Returns True if we are connected.
1332

1333
        Example:
1334

1335
            >>> s =  ssh(host='example.pwnme')
1336
            >>> s.connected()
1337
            True
1338
            >>> s.close()
1339
            >>> s.connected()
1340
            False
1341
        """
1342
        return bool(self.client and self.client.get_transport().is_active())
1✔
1343

1344
    def close(self):
1✔
1345
        """Close the connection."""
1346
        if self.client:
1!
1347
            self.client.close()
1✔
1348
            self.client = None
1✔
1349
            self.info("Closed connection to %r" % self.host)
1✔
1350

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

1369
        return misc.parse_ldd_output(packing._decode(data))
1✔
1370

1371
    def _get_fingerprint(self, remote):
1✔
1372
        cmd = '(sha256 || sha256sum || openssl sha256) 2>/dev/null < '
1✔
1373
        cmd = cmd + sh_string(remote)
1✔
1374

1375
        data, status = self.run_to_end(cmd)
1✔
1376

1377
        if status != 0:
1!
1378
            return None
×
1379

1380
        # OpenSSL outputs in the format of...
1381
        # (stdin)= e3b0c4429...
1382
        data = data.replace(b'(stdin)= ',b'')
1✔
1383

1384
        # sha256 and sha256sum outputs in the format of...
1385
        # e3b0c442...  -
1386
        data = data.replace(b'-',b'').strip()
1✔
1387

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

1391
        return data
1✔
1392

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

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

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

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

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

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

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

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

1430
        total = int(total)
1✔
1431

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

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

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

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

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

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

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

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

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

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

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

1479
        return local
1✔
1480

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

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

1487

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

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

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

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

1511
        Arguments:
1512
            remote(str): The remote filename to download
1513
            local(str): The local filename to save it to. Default is to infer it from the remote filename.
1514
        """
1515

1516

1517
        if not local:
1✔
1518
            local = os.path.basename(os.path.normpath(remote))
1✔
1519

1520
        if os.path.basename(remote) == remote:
1!
1521
            remote = os.path.join(self.cwd, remote)
×
1522

1523
        with self.progress('Downloading %r to %r' % (remote, local)) as p:
1✔
1524
            local_tmp = self._download_to_cache(remote, p)
1✔
1525

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

1530
    def download_dir(self, remote=None, local=None):
1✔
1531
        """Recursively downloads a directory from the remote server
1532

1533
        Arguments:
1534
            local: Local directory
1535
            remote: Remote directory
1536
        """
1537
        remote = packing._encode(remote or self.cwd)
×
1538

1539
        if self.sftp:
×
1540
            remote = packing._encode(self.sftp.normalize(remote))
×
1541
        else:
1542
            with context.local(log_level='error'):
×
1543
                remote = self.system(b'readlink -f ' + sh_string(remote))
×
1544

1545
        local = local or '.'
×
1546
        local = os.path.expanduser(local)
×
1547

1548
        self.info("Downloading %r to %r" % (remote, local))
×
1549

1550
        with context.local(log_level='error'):
×
1551
            remote_tar = self.mktemp()
×
1552
            cmd = b'tar -C %s -czf %s .' % \
×
1553
                  (sh_string(remote),
1554
                   sh_string(remote_tar))
1555
            tar = self.system(cmd)
×
1556

1557
            if 0 != tar.wait():
×
1558
                self.error("Could not create remote tar")
×
1559

1560
            local_tar = tempfile.NamedTemporaryFile(suffix='.tar.gz')
×
1561
            self.download_file(remote_tar, local_tar.name)
×
1562

1563
            tar = tarfile.open(local_tar.name)
×
1564
            tar.extractall(local)
×
1565

1566

1567
    def upload_data(self, data, remote):
1✔
1568
        """Uploads some data into a file on the remote server.
1569

1570
        Arguments:
1571
            data(str): The data to upload.
1572
            remote(str): The filename to upload it to.
1573

1574
        Example:
1575
            >>> s =  ssh(host='example.pwnme')
1576
            >>> s.upload_data(b'Hello, world', '/tmp/upload_foo')
1577
            >>> print(open('/tmp/upload_foo').read())
1578
            Hello, world
1579
            >>> s._sftp = False
1580
            >>> s._tried_sftp = True
1581
            >>> s.upload_data(b'Hello, world', '/tmp/upload_bar')
1582
            >>> print(open('/tmp/upload_bar').read())
1583
            Hello, world
1584
        """
1585
        data = packing._need_bytes(data)
1✔
1586
        # If a relative path was provided, prepend the cwd
1587
        if os.path.normpath(remote) == os.path.basename(remote):
1✔
1588
            remote = os.path.join(self.cwd, remote)
1✔
1589

1590
        if self.sftp:
1✔
1591
            with tempfile.NamedTemporaryFile() as f:
1✔
1592
                f.write(data)
1✔
1593
                f.flush()
1✔
1594
                self.sftp.put(f.name, remote)
1✔
1595
                return
1✔
1596

1597
        with context.local(log_level = 'ERROR'):
1✔
1598
            cmd = 'cat > ' + sh_string(remote)
1✔
1599
            s = self.run(cmd, tty=False)
1✔
1600
            s.send(data)
1✔
1601
            s.shutdown('send')
1✔
1602
            data   = s.recvall()
1✔
1603
            result = s.wait()
1✔
1604
            if result != 0:
1!
1605
                self.error("Could not upload file %r (%r)\n%s" % (remote, result, data))
×
1606

1607
    def upload_file(self, filename, remote = None):
1✔
1608
        """Uploads a file to the remote server. Returns the remote filename.
1609

1610
        Arguments:
1611
        filename(str): The local filename to download
1612
        remote(str): The remote filename to save it to. Default is to infer it from the local filename."""
1613

1614

1615
        if remote is None:
1✔
1616
            remote = os.path.normpath(filename)
1✔
1617
            remote = os.path.basename(remote)
1✔
1618
            remote = os.path.join(self.cwd, remote)
1✔
1619

1620
        with open(filename, 'rb') as fd:
1✔
1621
            data = fd.read()
1✔
1622

1623
        self.info("Uploading %r to %r" % (filename,remote))
1✔
1624
        self.upload_data(data, remote)
1✔
1625

1626
        return remote
1✔
1627

1628
    def upload_dir(self, local, remote=None):
1✔
1629
        """Recursively uploads a directory onto the remote server
1630

1631
        Arguments:
1632
            local: Local directory
1633
            remote: Remote directory
1634
        """
1635

1636
        remote    = remote or self.cwd
×
1637

1638
        local     = os.path.expanduser(local)
×
1639
        dirname   = os.path.dirname(local)
×
1640
        basename  = os.path.basename(local)
×
1641

1642
        if not os.path.isdir(local):
×
1643
            self.error("%r is not a directory" % local)
×
1644

1645
        msg = "Uploading %r to %r" % (basename,remote)
×
1646
        with self.waitfor(msg):
×
1647
            # Generate a tarfile with everything inside of it
1648
            local_tar  = tempfile.mktemp()
×
1649
            with tarfile.open(local_tar, 'w:gz') as tar:
×
1650
                tar.add(local, basename)
×
1651

1652
            # Upload and extract it
1653
            with context.local(log_level='error'):
×
1654
                remote_tar = self.mktemp('--suffix=.tar.gz')
×
1655
                self.upload_file(local_tar, remote_tar)
×
1656

1657
                untar = self.run('cd %s && tar -xzf %s' % (remote, remote_tar))
×
1658
                message = untar.recvrepeat(2)
×
1659

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

1663
    def upload(self, file_or_directory, remote=None):
1✔
1664
        """upload(file_or_directory, remote=None)
1665

1666
        Upload a file or directory to the remote host.
1667

1668
        Arguments:
1669
            file_or_directory(str): Path to the file or directory to download.
1670
            remote(str): Local path to store the data.
1671
                By default, uses the working directory.
1672
        """
1673
        if isinstance(file_or_directory, str):
1!
1674
            file_or_directory = os.path.expanduser(file_or_directory)
1✔
1675
            file_or_directory = os.path.expandvars(file_or_directory)
1✔
1676

1677
        if os.path.isfile(file_or_directory):
1!
1678
            return self.upload_file(file_or_directory, remote)
1✔
1679

1680
        if os.path.isdir(file_or_directory):
×
1681
            return self.upload_dir(file_or_directory, remote)
×
1682

1683
        self.error('%r does not exist' % file_or_directory)
×
1684

1685
    def download(self, file_or_directory, local=None):
1✔
1686
        """download(file_or_directory, local=None)
1687

1688
        Download a file or directory from the remote host.
1689

1690
        Arguments:
1691
            file_or_directory(str): Path to the file or directory to download.
1692
            local(str): Local path to store the data.
1693
                By default, uses the current directory.
1694
        """
1695
        file_or_directory = packing._encode(file_or_directory)
×
1696
        with self.system(b'test -d ' + sh_string(file_or_directory)) as io:
×
1697
            is_dir = io.wait()
×
1698

1699
        if 0 == is_dir:
×
1700
            self.download_dir(file_or_directory, local)
×
1701
        else:
1702
            self.download_file(file_or_directory, local)
×
1703

1704
    put = upload
1✔
1705
    get = download
1✔
1706

1707
    def unlink(self, file):
1✔
1708
        """unlink(file)
1709

1710
        Delete the file on the remote host
1711

1712
        Arguments:
1713
            file(str): Path to the file
1714
        """
1715
        if not self.sftp:
×
1716
            self.error("unlink() is only supported if SFTP is supported")
×
1717

1718
        return self.sftp.unlink(file)
×
1719

1720
    def libs(self, remote, directory = None):
1✔
1721
        """Downloads the libraries referred to by a file.
1722

1723
        This is done by running ldd on the remote server, parsing the output
1724
        and downloading the relevant files.
1725

1726
        The directory argument specified where to download the files. This defaults
1727
        to './$HOSTNAME' where $HOSTNAME is the hostname of the remote server."""
1728

1729
        libs = self._libs_remote(remote)
1✔
1730

1731
        remote = packing._decode(self.readlink('-f',remote).strip())
1✔
1732
        libs[remote] = 0
1✔
1733

1734
        if directory is None:
1!
1735
            directory = self.host
1✔
1736

1737
        directory = os.path.realpath(directory)
1✔
1738

1739
        res = {}
1✔
1740

1741
        seen = set()
1✔
1742

1743
        for lib, addr in libs.items():
1✔
1744
            local = os.path.realpath(os.path.join(directory, '.' + os.path.sep + lib))
1✔
1745
            if not local.startswith(directory):
1!
1746
                self.warning('This seems fishy: %r' % lib)
×
1747
                continue
×
1748

1749
            misc.mkdir_p(os.path.dirname(local))
1✔
1750

1751
            if lib not in seen:
1!
1752
                self.download_file(lib, local)
1✔
1753
                seen.add(lib)
1✔
1754
            res[local] = addr
1✔
1755

1756
        return res
1✔
1757

1758
    def interactive(self, shell=None):
1✔
1759
        """Create an interactive session.
1760

1761
        This is a simple wrapper for creating a new
1762
        :class:`pwnlib.tubes.ssh.ssh_channel` object and calling
1763
        :meth:`pwnlib.tubes.ssh.ssh_channel.interactive` on it."""
1764

1765
        s = self.shell(shell)
×
1766

1767
        if self.cwd != '.':
×
1768
            cmd = 'cd ' + sh_string(self.cwd)
×
1769
            s.sendline(cmd)
×
1770

1771
        s.interactive()
×
1772
        s.close()
×
1773

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

1779
        Note:
1780
            This uses ``mktemp -d`` under the covers, sets permissions
1781
            on the directory to ``0700``.  This means that setuid binaries
1782
            will **not** be able to access files created in this directory.
1783

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

1786
        Arguments:
1787
            wd(string): Working directory.  Default is to auto-generate a directory
1788
                based on the result of running 'mktemp -d' on the remote machine.
1789
            symlink(bool,str): Create symlinks in the new directory.
1790

1791
                The default value, ``False``, implies that no symlinks should be
1792
                created.
1793

1794
                A string value is treated as a path that should be symlinked.
1795
                It is passed directly to the shell on the remote end for expansion,
1796
                so wildcards work.
1797

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

1801
        Examples:
1802
            >>> s =  ssh(host='example.pwnme')
1803
            >>> cwd = s.set_working_directory()
1804
            >>> s.ls()
1805
            b''
1806
            >>> packing._decode(s.pwd()) == cwd
1807
            True
1808

1809
            >>> s =  ssh(host='example.pwnme')
1810
            >>> homedir = s.pwd()
1811
            >>> _=s.touch('foo')
1812

1813
            >>> _=s.set_working_directory()
1814
            >>> assert s.ls() == b''
1815

1816
            >>> _=s.set_working_directory(homedir)
1817
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1818

1819
            >>> _=s.set_working_directory(symlink=True)
1820
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1821
            >>> assert homedir != s.pwd()
1822

1823
            >>> symlink=os.path.join(homedir,b'*')
1824
            >>> _=s.set_working_directory(symlink=symlink)
1825
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1826
            >>> assert homedir != s.pwd()
1827
        """
1828
        status = 0
1✔
1829

1830
        if symlink and not isinstance(symlink, (six.binary_type, six.text_type)):
1✔
1831
            symlink = os.path.join(self.pwd(), b'*')
1✔
1832
        if not hasattr(symlink, 'encode') and hasattr(symlink, 'decode'):
1!
1833
            symlink = symlink.decode('utf-8')
×
1834
            
1835
        if isinstance(wd, six.text_type):
1!
1836
            wd = packing._need_bytes(wd, 2, 0x80)
×
1837

1838
        if not wd:
1✔
1839
            wd, status = self.run_to_end('x=$(mktemp -d) && cd $x && chmod +x . && echo $PWD', wd='.')
1✔
1840
            wd = wd.strip()
1✔
1841

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

1845
        else:
1846
            cmd = b'ls ' + sh_string(wd)
1✔
1847
            _, status = self.run_to_end(cmd, wd = '.')
1✔
1848

1849
            if status:
1!
1850
                self.error("%r does not appear to exist" % wd)
×
1851

1852
        if not isinstance(wd, str):
1!
1853
            wd = wd.decode('utf-8')
×
1854
        self.cwd = wd
1✔
1855

1856
        self.info("Working directory: %r" % self.cwd)
1✔
1857

1858
        if symlink:
1✔
1859
            self.ln('-s', symlink, '.')
1✔
1860

1861
        return wd
1✔
1862

1863
    def write(self, path, data):
1✔
1864
        """Wrapper around upload_data to match :func:`pwnlib.util.misc.write`"""
1865
        data = packing._need_bytes(data)
1✔
1866
        return self.upload_data(data, path)
1✔
1867

1868
    def read(self, path):
1✔
1869
        """Wrapper around download_data to match :func:`pwnlib.util.misc.read`"""
1870
        return self.download_data(path)
1✔
1871

1872
    def _init_remote_platform_info(self):
1✔
1873
        r"""Fills _platform_info, e.g.:
1874

1875
        ::
1876

1877
            {'distro': 'Ubuntu\n',
1878
             'distro_ver': '14.04\n',
1879
             'machine': 'x86_64',
1880
             'node': 'pwnable.kr',
1881
             'processor': 'x86_64',
1882
             'release': '3.11.0-12-generic',
1883
             'system': 'linux',
1884
             'version': '#19-ubuntu smp wed oct 9 16:20:46 utc 2013'}
1885
        """
1886
        if self._platform_info:
1✔
1887
            return
1✔
1888

1889
        def preexec():
1✔
1890
            import platform
×
1891
            print('\n'.join(platform.uname()))
×
1892

1893
        with context.quiet:
1✔
1894
            with self.process('true', preexec_fn=preexec) as io:
1✔
1895

1896
                self._platform_info = {
1✔
1897
                    'system': io.recvline().lower().strip().decode(),
1898
                    'node': io.recvline().lower().strip().decode(),
1899
                    'release': io.recvline().lower().strip().decode(),
1900
                    'version': io.recvline().lower().strip().decode(),
1901
                    'machine': io.recvline().lower().strip().decode(),
1902
                    'processor': io.recvline().lower().strip().decode(),
1903
                    'distro': 'Unknown',
1904
                    'distro_ver': ''
1905
                }
1906

1907
            try:
1✔
1908
                if not self.which('lsb_release'):
1!
1909
                    return
×
1910

1911
                with self.process(['lsb_release', '-irs']) as io:
1✔
1912
                    lsb_info = io.recvall().strip().decode()
1✔
1913
                    self._platform_info['distro'], self._platform_info['distro_ver'] = lsb_info.split()
1✔
1914
            except Exception:
×
1915
                pass
×
1916

1917
    @property
1✔
1918
    def os(self):
1919
        """:class:`str`: Operating System of the remote machine."""
1920
        try:
1✔
1921
            self._init_remote_platform_info()
1✔
1922
            with context.local(os=self._platform_info['system']):
1✔
1923
                return context.os
1✔
1924
        except Exception:
×
1925
            return "Unknown"
×
1926

1927

1928
    @property
1✔
1929
    def arch(self):
1930
        """:class:`str`: CPU Architecture of the remote machine."""
1931
        try:
1✔
1932
            self._init_remote_platform_info()
1✔
1933
            with context.local(arch=self._platform_info['machine']):
1✔
1934
                return context.arch
1✔
1935
        except Exception:
×
1936
            return "Unknown"
×
1937

1938
    @property
1✔
1939
    def bits(self):
1940
        """:class:`str`: Pointer size of the remote machine."""
1941
        try:
×
1942
            with context.local():
×
1943
                context.clear()
×
1944
                context.arch = self.arch
×
1945
                return context.bits
×
1946
        except Exception:
×
1947
            return context.bits
×
1948

1949
    @property
1✔
1950
    def version(self):
1951
        """:class:`tuple`: Kernel version of the remote machine."""
1952
        try:
1✔
1953
            self._init_remote_platform_info()
1✔
1954
            vers = self._platform_info['release']
1✔
1955

1956
            # 3.11.0-12-generic
1957
            expr = r'([0-9]+\.?)+'
1✔
1958

1959
            vers = re.search(expr, vers).group()
1✔
1960
            return tuple(map(int, vers.split('.')))
1✔
1961

1962
        except Exception:
×
1963
            return (0,0,0)
×
1964

1965
    @property
1✔
1966
    def distro(self):
1967
        """:class:`tuple`: Linux distribution name and release."""
1968
        try:
1✔
1969
            self._init_remote_platform_info()
1✔
1970
            return (self._platform_info['distro'], self._platform_info['distro_ver'])
1✔
1971
        except Exception:
×
1972
            return ("Unknown", "Unknown")
×
1973

1974
    @property
1✔
1975
    def aslr(self):
1976
        """:class:`bool`: Whether ASLR is enabled on the system.
1977

1978
        Example:
1979

1980
            >>> s = ssh("travis", "example.pwnme")
1981
            >>> s.aslr
1982
            True
1983
        """
1984
        if self._aslr is None:
1!
1985
            if self.os != 'linux':
1!
1986
                self.warn_once("Only Linux is supported for ASLR checks.")
×
1987
                self._aslr = False
×
1988

1989
            else:
1990
                with context.quiet:
1✔
1991
                    rvs = self.read('/proc/sys/kernel/randomize_va_space')
1✔
1992

1993
                self._aslr = not rvs.startswith(b'0')
1✔
1994

1995
        return self._aslr
1✔
1996

1997
    @property
1✔
1998
    def aslr_ulimit(self):
1999
        """:class:`bool`: Whether the entropy of 32-bit processes can be reduced with ulimit."""
2000
        import pwnlib.elf.elf
1✔
2001
        import pwnlib.shellcraft
1✔
2002

2003
        if self._aslr_ulimit is not None:
1!
2004
            return self._aslr_ulimit
×
2005

2006
        # This test must run a 32-bit binary, fix the architecture
2007
        arch = {
1✔
2008
            'amd64': 'i386',
2009
            'aarch64': 'arm'
2010
        }.get(self.arch, self.arch)
2011

2012
        with context.local(arch=arch, bits=32, os=self.os, aslr=True):
1✔
2013
            with context.quiet:
1✔
2014
                try:
1✔
2015
                    sc = pwnlib.shellcraft.cat('/proc/self/maps') \
1✔
2016
                       + pwnlib.shellcraft.exit(0)
2017

2018
                    elf = pwnlib.elf.elf.ELF.from_assembly(sc, shared=True)
1✔
2019
                except Exception:
×
2020
                    self.warn_once("Can't determine ulimit ASLR status")
×
2021
                    self._aslr_ulimit = False
×
2022
                    return self._aslr_ulimit
×
2023

2024
                def preexec():
1✔
2025
                    import resource
×
2026
                    try:
×
2027
                        resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
2028
                    except Exception:
×
2029
                        pass
×
2030

2031
                # Move to a new temporary directory
2032
                cwd = self.cwd
1✔
2033
                tmp = self.set_working_directory()
1✔
2034

2035
                try:
1✔
2036
                    self.upload(elf.path, './aslr-test')
1✔
2037
                except IOError:
×
2038
                    self.warn_once("Couldn't check ASLR ulimit trick")
×
2039
                    self._aslr_ulimit = False
×
2040
                    return False
×
2041

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

2045
                # Move back to the old directory
2046
                self.cwd = cwd
1✔
2047

2048
                # Clean up the files
2049
                self.process(['rm', '-rf', tmp]).wait()
1✔
2050

2051
        # Check for 555555000 (1/3 of the address space for PAE)
2052
        # and for 40000000 (1/3 of the address space with 3BG barrier)
2053
        self._aslr_ulimit = bool(b'55555000' in maps or b'40000000' in maps)
1✔
2054

2055
        return self._aslr_ulimit
1✔
2056

2057
    def _checksec_cache(self, value=None):
1✔
2058
        path = self._get_cachefile('%s-%s' % (self.host, self.port))
1✔
2059

2060
        if value is not None:
1✔
2061
            with open(path, 'w+') as f:
1✔
2062
                f.write(value)
1✔
2063
        elif os.path.exists(path):
1✔
2064
            with open(path, 'r+') as f:
1✔
2065
                return f.read()
1✔
2066

2067
    def checksec(self, banner=True):
1✔
2068
        """checksec()
2069

2070
        Prints a helpful message about the remote system.
2071

2072
        Arguments:
2073
            banner(bool): Whether to print the path to the ELF binary.
2074
        """
2075
        cached = self._checksec_cache()
1✔
2076
        if cached:
1✔
2077
            return cached
1✔
2078

2079
        red    = text.red
1✔
2080
        green  = text.green
1✔
2081
        yellow = text.yellow
1✔
2082

2083
        res = [
1✔
2084
            "%s@%s:" % (self.user, self.host),
2085
            "Distro".ljust(10) + ' '.join(self.distro),
2086
            "OS:".ljust(10) + self.os,
2087
            "Arch:".ljust(10) + self.arch,
2088
            "Version:".ljust(10) + '.'.join(map(str, self.version)),
2089

2090
            "ASLR:".ljust(10) + {
2091
                True: green("Enabled"),
2092
                False: red("Disabled")
2093
            }[self.aslr]
2094
        ]
2095

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

2099
        cached = '\n'.join(res)
1✔
2100
        self._checksec_cache(cached)
1✔
2101
        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

© 2024 Coveralls, Inc