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

Gallopsled / pwntools / 9051549415

12 May 2024 12:55PM UTC coverage: 74.067% (-0.02%) from 74.089%
9051549415

Pull #2405

github

web-flow
Merge 5a065fc65 into e92a30bbf
Pull Request #2405: Add "none" ssh authentication method

4466 of 7249 branches covered (61.61%)

0 of 4 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

13075 of 17653 relevant lines covered (74.07%)

0.74 hits per line

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

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

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

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

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

37
class ssh_channel(sock):
1✔
38

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

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

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

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

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

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

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

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

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

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

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

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

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

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

99

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

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

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

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

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

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

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

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

140
            h.success()
1✔
141

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

145
        Kills the process.
146
        """
147

148
        self.close()
×
149

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

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

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

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

171
        return data
1✔
172

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

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

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

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

190
        return self.returncode
1✔
191

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

333

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

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

342
        Examples:
343

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

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

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

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

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

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

372

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

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

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

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

386
        Examples:
387

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

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

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

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

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

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

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

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

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

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

436

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

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

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

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

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

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

469
            h.success()
1✔
470

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

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

477

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

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

485
        self.host = parent.host
1✔
486

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

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

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

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

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

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

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

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

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

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

538

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

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

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

547
    #: Remote username (``str``)
548
    user = None
1✔
549

550
    #: Remote password (``str``)
551
    password = None
1✔
552

553
    #: Remote private key (``str``)
554
    key = None
1✔
555

556
    #: Remote private key file (``str``)
557
    keyfile = None
1✔
558

559
    #: Enable caching of SSH downloads (``bool``)
560
    cache = True
1✔
561

562
    #: Enable raw mode and don't probe the environment (``bool``)
563
    raw = False
1✔
564

565
    #: Paramiko SSHClient which backs this object
566
    client = None
1✔
567

568
    #: PID of the remote ``sshd`` process servicing this connection.
569
    pid = None
1✔
570

571
    _cwd = '.'
1✔
572
    _tried_sftp = False
1✔
573

574
    def __init__(self, user=None, host=None, port=22, password=None, key=None,
1✔
575
                 keyfile=None, proxy_command=None, proxy_sock=None, level=None,
576
                 cache=True, ssh_agent=False, ignore_config=False, raw=False, 
577
                 auth_none=False, *a, **kw):
578
        """Creates a new ssh connection.
579

580
        Arguments:
581
            user(str): The username to log in with
582
            host(str): The hostname to connect to
583
            port(int): The port to connect to
584
            password(str): Try to authenticate using this password
585
            key(str): Try to authenticate using this private key. The string should be the actual private key.
586
            keyfile(str): Try to authenticate using this private key. The string should be a filename.
587
            proxy_command(str): Use this as a proxy command. It has approximately the same semantics as ProxyCommand from ssh(1).
588
            proxy_sock(str): Use this socket instead of connecting to the host.
589
            timeout: Timeout, in seconds
590
            level: Log level
591
            cache(bool): Cache downloaded files (by hash/size/timestamp)
592
            ssh_agent(bool): If :const:`True`, enable usage of keys via ssh-agent
593
            ignore_config(bool): If :const:`True`, disable usage of ~/.ssh/config and ~/.ssh/authorized_keys
594
            raw(bool): If :const:`True`, assume a non-standard shell and don't probe the environment
595
            auth_none(bool): If :const:`True`, try to authenticate with no authentication methods
596

597
        NOTE: The proxy_command and proxy_sock arguments is only available if a
598
        fairly new version of paramiko is used.
599

600
        Example proxying:
601

602
        .. doctest::
603
           :skipif: True
604

605
            >>> s1 = ssh(host='example.pwnme')
606
            >>> r1 = s1.remote('localhost', 22)
607
            >>> s2 = ssh(host='example.pwnme', proxy_sock=r1.sock)
608
            >>> r2 = s2.remote('localhost', 22) # and so on...
609
            >>> for x in r2, s2, r1, s1: x.close()
610
        """
611
        super(ssh, self).__init__(*a, **kw)
1✔
612

613
        Logger.__init__(self)
1✔
614
        if level is not None:
1!
615
            self.setLevel(level)
×
616

617

618
        self.host            = host
1✔
619
        self.port            = port
1✔
620
        self.user            = user
1✔
621
        self.password        = password
1✔
622
        self.key             = key
1✔
623
        self.keyfile         = keyfile
1✔
624
        self._cachedir       = os.path.join(tempfile.gettempdir(), 'pwntools-ssh-cache')
1✔
625
        self.cache           = cache
1✔
626
        self.raw             = raw
1✔
627

628
        # Deferred attributes
629
        self._platform_info = {}
1✔
630
        self._aslr = None
1✔
631
        self._aslr_ulimit = None
1✔
632
        self._cpuinfo_cache = None
1✔
633
        self._user_shstk = None
1✔
634
        self._ibt = None
1✔
635

636
        misc.mkdir_p(self._cachedir)
1✔
637

638
        import paramiko
1✔
639

640
        # Make a basic attempt to parse the ssh_config file
641
        try:
1✔
642
            config_file = os.path.expanduser('~/.ssh/config')
1✔
643

644
            if not ignore_config and os.path.exists(config_file):
1!
645
                ssh_config  = paramiko.SSHConfig()
1✔
646
                ssh_config.parse(open(config_file))
1✔
647
                host_config = ssh_config.lookup(host)
1✔
648
                if 'hostname' in host_config:
1!
649
                    self.host = host = host_config['hostname']
1✔
650
                if not user and 'user' in host_config:
1✔
651
                    self.user = user = host_config['user']
1✔
652
                if not keyfile and 'identityfile' in host_config:
1!
653
                    keyfile = host_config['identityfile'][0]
1✔
654
                    if keyfile.lower() == 'none':
1!
655
                        keyfile = None
×
656
        except Exception as e:
×
657
            self.debug("An error occurred while parsing ~/.ssh/config:\n%s" % e)
×
658

659
        keyfiles = [os.path.expanduser(keyfile)] if keyfile else []
1✔
660

661
        msg = 'Connecting to %s on port %d' % (host, port)
1✔
662
        with self.waitfor(msg) as h:
1✔
663
            self.client = paramiko.SSHClient()
1✔
664
            self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1✔
665

666
            if not ignore_config:
1!
667
                known_hosts = os.path.expanduser('~/.ssh/known_hosts')
1✔
668
                if os.path.exists(known_hosts):
1!
669
                    self.client.load_host_keys(known_hosts)
1✔
670

671
            has_proxy = bool(proxy_sock or proxy_command)
1✔
672
            if has_proxy:
1!
673
                if 'ProxyCommand' not in dir(paramiko):
×
674
                    self.error('This version of paramiko does not support proxies.')
×
675

676
                if proxy_sock and proxy_command:
×
677
                    self.error('Cannot have both a proxy command and a proxy sock')
×
678

679
                if proxy_command:
×
680
                    proxy_sock = paramiko.ProxyCommand(proxy_command)
×
681
            else:
682
                proxy_sock = None
1✔
683

684
            try:
1✔
685
                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✔
686
            except paramiko.BadHostKeyException as e:
×
687
                self.error("Remote host %(host)s is using a different key than stated in known_hosts\n"
×
688
                           "    To remove the existing entry from your known_hosts and trust the new key, run the following commands:\n"
689
                           "        $ ssh-keygen -R %(host)s\n"
690
                           "        $ ssh-keygen -R [%(host)s]:%(port)s" % locals())
NEW
691
            except paramiko.SSHException as e:
×
NEW
692
                if user and auth_none and str(e) == "No authentication methods available":
×
NEW
693
                    self.client.get_transport().auth_none(user)
×
694
                else:
NEW
695
                    raise
×
696

697
            self.transport = self.client.get_transport()
1✔
698
            self.transport.use_compression(True)
1✔
699

700
            h.success()
1✔
701

702
        if self.raw:
1!
703
            return
×
704

705
        self._tried_sftp = False
1✔
706

707
        if self.sftp:
1!
708
            with context.quiet:
1✔
709
                self.cwd = packing._decode(self.pwd())
1✔
710
        else:
711
            self.cwd = '.'
×
712

713
        with context.local(log_level='error'):
1✔
714
            def getppid():
1✔
715
                print(os.getppid())
×
716
            try:
1✔
717
                self.pid = int(self.process('false', preexec_fn=getppid).recvall())
1✔
718
            except Exception:
×
719
                self.pid = None
×
720

721
        try:
1✔
722
            self.info_once(self.checksec())
1✔
723
        except Exception:
×
724
            self.warn_once("Couldn't check security settings on %r" % self.host)
×
725

726
    def __repr__(self):
1✔
727
        return "{}(user={!r}, host={!r})".format(self.__class__.__name__, self.user, self.host)
1✔
728

729
    @property
1✔
730
    def cwd(self):
1✔
731
        return self._cwd
1✔
732

733
    @cwd.setter
1✔
734
    def cwd(self, cwd):
1✔
735
        self._cwd = cwd
1✔
736
        if self.sftp:
1!
737
            self.sftp.chdir(cwd)
1✔
738

739
    @property
1✔
740
    def sftp(self):
1✔
741
        """Paramiko SFTPClient object which is used for file transfers.
742
        Set to :const:`None` to disable ``sftp``.
743
        """
744
        if not self._tried_sftp:
1✔
745
            try:
1✔
746
                self._sftp = self.transport.open_sftp_client()
1✔
747
            except Exception:
×
748
                self._sftp = None
×
749

750
        self._tried_sftp = True
1✔
751
        return self._sftp
1✔
752

753
    @sftp.setter
1✔
754
    def sftp(self, value):
1✔
755
        self._sftp = value
×
756
        self._tried_sftp = True
×
757

758
    def __enter__(self, *a):
1✔
759
        return self
×
760

761
    def __exit__(self, *a, **kw):
1✔
762
        self.close()
×
763

764
    def shell(self, shell = None, tty = True, timeout = Timeout.default):
1✔
765
        """shell(shell = None, tty = True, timeout = Timeout.default) -> ssh_channel
766

767
        Open a new channel with a shell inside.
768

769
        Arguments:
770
            shell(str): Path to the shell program to run.
771
                If :const:`None`, uses the default shell for the logged in user.
772
            tty(bool): If :const:`True`, then a TTY is requested on the remote server.
773

774
        Returns:
775
            Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
776

777
        Examples:
778

779
            >>> s =  ssh(host='example.pwnme')
780
            >>> sh = s.shell('/bin/sh')
781
            >>> sh.sendline(b'echo Hello; exit')
782
            >>> print(b'Hello' in sh.recvall())
783
            True
784
        """
785
        return self.run(shell, tty, timeout = timeout)
1✔
786

787
    def process(self, argv=None, executable=None, tty=True, cwd=None, env=None, ignore_environ=None, timeout=Timeout.default, run=True,
1✔
788
                stdin=0, stdout=1, stderr=2, preexec_fn=None, preexec_args=(), raw=True, aslr=None, setuid=None,
789
                shell=False):
790
        r"""
791
        Executes a process on the remote server, in the same fashion
792
        as pwnlib.tubes.process.process.
793

794
        To achieve this, a Python script is created to call ``os.execve``
795
        with the appropriate arguments.
796

797
        As an added bonus, the ``ssh_channel`` object returned has a
798
        ``pid`` property for the process pid.
799

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

850
        Returns:
851
            A new SSH channel, or a path to a script if ``run=False``.
852

853
        Notes:
854
            Requires Python on the remote server.
855

856
        Examples:
857

858
            >>> s = ssh(host='example.pwnme')
859
            >>> sh = s.process('/bin/sh', env={'PS1':''})
860
            >>> sh.sendline(b'echo Hello; exit')
861
            >>> sh.recvall()
862
            b'Hello\n'
863
            >>> s.process(['/bin/echo', b'\xff']).recvall()
864
            b'\xff\n'
865
            >>> s.process(['readlink', '/proc/self/exe']).recvall() # doctest: +ELLIPSIS
866
            b'.../bin/readlink\n'
867
            >>> s.process(['LOLOLOL', '/proc/self/exe'], executable='readlink').recvall() # doctest: +ELLIPSIS
868
            b'.../bin/readlink\n'
869
            >>> s.process(['LOLOLOL\x00', '/proc/self/cmdline'], executable='cat').recvall()
870
            b'LOLOLOL\x00/proc/self/cmdline\x00'
871
            >>> sh = s.process(executable='/bin/sh')
872
            >>> str(sh.pid).encode() in s.pidof('sh') # doctest: +SKIP
873
            True
874
            >>> io = s.process(['pwd'], cwd='/tmp')
875
            >>> io.recvall()
876
            b'/tmp\n'
877
            >>> io.cwd
878
            '/tmp'
879
            >>> p = s.process(['python','-c','import os; os.write(1, os.read(2, 1024))'], stderr=0)
880
            >>> p.send(b'hello')
881
            >>> p.recv()
882
            b'hello'
883
            >>> s.process(['/bin/echo', 'hello']).recvall()
884
            b'hello\n'
885
            >>> s.process(['/bin/echo', 'hello'], stdout='/dev/null').recvall()
886
            b''
887
            >>> s.process(['/usr/bin/env'], env={}).recvall()
888
            b''
889
            >>> s.process('/usr/bin/env', env={'A':'B'}).recvall()
890
            b'A=B\n'
891

892
            >>> s.process('false', preexec_fn=1234)
893
            Traceback (most recent call last):
894
            ...
895
            PwnlibException: preexec_fn must be a function
896

897
            >>> s.process('false', preexec_fn=lambda: 1234)
898
            Traceback (most recent call last):
899
            ...
900
            PwnlibException: preexec_fn cannot be a lambda
901

902
            >>> def uses_globals():
903
            ...     foo = bar
904
            >>> print(s.process('false', preexec_fn=uses_globals).recvall().strip().decode()) # doctest: +ELLIPSIS
905
            Traceback (most recent call last):
906
            ...
907
            NameError: ...name 'bar' is not defined
908

909
            >>> s.process('echo hello', shell=True).recvall()
910
            b'hello\n'
911

912
            >>> io = s.process(['cat'], timeout=5)
913
            >>> io.recvline()
914
            b''
915

916
            >>> # Testing that empty argv works
917
            >>> io = s.process([], executable='sh')
918
            >>> io.sendline(b'echo $0')
919
            >>> io.recvline()
920
            b'$ \n'
921
            >>> # Make sure that we have a shell
922
            >>> io.sendline(b'echo hello')
923
            >>> io.recvline()
924
            b'$ hello\n'
925

926
            >>> # Testing that empty argv[0] works
927
            >>> io = s.process([''], executable='sh')
928
            >>> io.sendline(b'echo $0')
929
            >>> io.recvline()
930
            b'$ \n'
931

932
        """
933
        cwd = cwd or self.cwd
1✔
934
        script = misc._create_execve_script(argv=argv, executable=executable,
1✔
935
                cwd=cwd, env=env, stdin=stdin, stdout=stdout, stderr=stderr,
936
                ignore_environ=ignore_environ, preexec_fn=preexec_fn, preexec_args=preexec_args,
937
                aslr=aslr, setuid=setuid, shell=shell, log=self)
938

939

940
        self.debug("Created execve script:\n" + script)
1✔
941

942
        if not run:
1✔
943
            with context.local(log_level='error'):
1✔
944
                tmpfile = self.mktemp('-t', 'pwnlib-execve-XXXXXXXXXX')
1✔
945
                self.chmod('+x', tmpfile)
1✔
946

947
            self.info("Uploading execve script to %r" % tmpfile)
1✔
948
            script = packing._encode(script)
1✔
949
            self.upload_data(script, tmpfile)
1✔
950
            return tmpfile
1✔
951

952
        if self.isEnabledFor(logging.DEBUG):
1!
953
            execve_repr = "execve(%r, %s, %s)" % (executable,
×
954
                                                  argv,
955
                                                  'os.environ'
956
                                                  if (env in (None, getattr(os, "environb", os.environ))) 
957
                                                  else env)
958
            # Avoid spamming the screen
959
            if self.isEnabledFor(logging.DEBUG) and len(execve_repr) > 512:
×
960
                execve_repr = execve_repr[:512] + '...'
×
961
        else:
962
            execve_repr = repr(executable)
1✔
963

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

966
        if timeout == Timeout.default:
1✔
967
            timeout = self.timeout
1✔
968

969
        with self.progress(msg) as h:
1!
970

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

975
            try:
1✔
976
                python.recvline_contains(b'PWNTOOLS')   # Magic flag so that any sh/bash initialization errors are swallowed
1✔
977
                try:
1✔
978
                    if b'python' not in python.recvline():  # Python interpreter that was selected
1!
979
                        raise ValueError("Python not found on remote host")
×
980
                except (EOFError, ValueError):
×
981
                    self.warn_once('Could not find a Python interpreter on %s\n' % self.host
×
982
                                   + "Use ssh.system() instead of ssh.process()\n")
983
                    h.failure("Process creation failed")
×
984
                    return None
×
985

986
                result = safeeval.const(python.recvline())  # Status flag from the Python script
1✔
987
            except (EOFError, ValueError):
×
988
                h.failure("Process creation failed")
×
989
                return None
×
990

991
            # If an error occurred, try to grab as much output
992
            # as we can.
993
            if result != 1:
1!
994
                error_message = python.recvrepeat(timeout=1)
×
995

996
            if result == 0:
1!
997
                self.error("%r does not exist or is not executable" % executable)
×
998
            elif result == 3:
1!
999
                self.error("%r" % error_message)
×
1000
            elif result == 2:
1!
1001
                self.error("python is not installed on the remote system %r" % self.host)
×
1002
            elif result != 1:
1!
1003
                h.failure("something bad happened:\n%s" % error_message)
×
1004

1005
            python.pid  = safeeval.const(python.recvline())
1✔
1006
            python.uid  = safeeval.const(python.recvline())
1✔
1007
            python.gid  = safeeval.const(python.recvline())
1✔
1008
            python.suid = safeeval.const(python.recvline())
1✔
1009
            python.sgid = safeeval.const(python.recvline())
1✔
1010
            python.argv = argv
1✔
1011
            python.executable = packing._decode(python.recvuntil(b'\x00')[:-1])
1✔
1012

1013
            h.success('pid %i' % python.pid)
1✔
1014

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

1020
            if self.aslr_ulimit:
×
1021
                message += "Unlimited stack size should de-randomize shared libraries."
×
1022

1023
            self.warn_once(message)
×
1024

1025
        elif not aslr:
1✔
1026
            self.warn_once("ASLR is disabled for %r!" % python.executable)
1✔
1027

1028
        return python
1✔
1029

1030
    def which(self, program):
1✔
1031
        """which(program) -> str
1032

1033
        Minor modification to just directly invoking ``which`` on the remote
1034
        system which adds the current working directory to the end of ``$PATH``.
1035
        """
1036
        # If name is a path, do not attempt to resolve it.
1037
        if os.path.sep in program:
1✔
1038
            return program
1✔
1039

1040
        program = packing._encode(program)
1✔
1041

1042
        result = self.system(b'export PATH=$PATH:$PWD; command -v ' + program).recvall().strip()
1✔
1043

1044
        if (b'/' + program) not in result:
1✔
1045
            return None
1✔
1046

1047
        return packing._decode(result)
1✔
1048

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

1052
        Open a new channel with a specific process inside. If `tty` is True,
1053
        then a TTY is requested on the remote server.
1054

1055
        If `raw` is True, terminal control codes are ignored and input is not
1056
        echoed back.
1057

1058
        Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
1059

1060
        Examples:
1061

1062
            >>> s =  ssh(host='example.pwnme')
1063
            >>> py = s.system('python3 -i')
1064
            >>> _ = py.recvuntil(b'>>> ')
1065
            >>> py.sendline(b'print(2+2)')
1066
            >>> py.sendline(b'exit()')
1067
            >>> print(repr(py.recvline()))
1068
            b'4\n'
1069
            >>> s.system('env | grep -a AAAA', env={'AAAA': b'\x90'}).recvall()
1070
            b'AAAA=\x90\n'
1071
            >>> io = s.system('pwd', cwd='/tmp')
1072
            >>> io.recvall()
1073
            b'/tmp\n'
1074
            >>> io.cwd
1075
            '/tmp'
1076
        """
1077
        if wd is not None:
1!
1078
            self.warning_once("The 'wd' argument to ssh.system() is deprecated.  Use 'cwd' instead.")
×
1079
            if cwd is None:
×
1080
                cwd = wd
×
1081
        if cwd is None:
1✔
1082
            cwd = self.cwd
1✔
1083

1084
        if timeout is None:
1✔
1085
            timeout = self.timeout
1✔
1086

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

1089
    #: Backward compatibility.  Use :meth:`system`
1090
    run = system
1✔
1091

1092
    def getenv(self, variable, **kwargs):
1✔
1093
        """Retrieve the address of an environment variable on the remote
1094
        system.
1095

1096
        Note:
1097

1098
            The exact address will differ based on what other environment
1099
            variables are set, as well as argv[0].  In order to ensure that
1100
            the path is *exactly* the same, it is recommended to invoke the
1101
            process with ``argv=[]``.
1102
        """
1103
        script = '''
×
1104
from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r))
1105
''' % variable
1106

1107
        with context.local(log_level='error'):
×
1108
            python = self.which('python') or self.which('python2.7') or self.which('python3')
×
1109

1110
            if not python:
×
1111
                self.error("Python is not installed on the remote system.")
×
1112

1113
            io = self.process(['','-c', script.strip()], executable=python, **kwargs)
×
1114
            result = io.recvall()
×
1115

1116
        try:
×
1117
            return int(result) & context.mask
×
1118
        except ValueError:
×
1119
            self.exception("Could not look up environment variable %r" % variable)
×
1120

1121

1122

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

1126
        Run a command on the remote server and return a tuple with
1127
        (data, exit_status). If `tty` is True, then the command is run inside
1128
        a TTY on the remote server.
1129

1130
        Examples:
1131

1132
            >>> s =  ssh(host='example.pwnme')
1133
            >>> print(s.run_to_end('echo Hello; exit 17'))
1134
            (b'Hello\n', 17)
1135
            """
1136

1137
        if wd is not None:
1✔
1138
            self.warning_once("The 'wd' argument to ssh.run_to_end() is deprecated.  Use 'cwd' instead.")
1✔
1139
            if cwd is None:
1!
1140
                cwd = wd
1✔
1141

1142
        with context.local(log_level = 'ERROR'):
1✔
1143
            c = self.run(process, tty, cwd = cwd, env = env, timeout = Timeout.default)
1✔
1144
            data = c.recvall()
1✔
1145
            retcode = c.wait()
1✔
1146
            c.close()
1✔
1147
            return data, retcode
1✔
1148

1149
    def connect_remote(self, host, port, timeout = Timeout.default):
1✔
1150
        r"""connect_remote(host, port, timeout = Timeout.default) -> ssh_connecter
1151

1152
        Connects to a host through an SSH connection. This is equivalent to
1153
        using the ``-L`` flag on ``ssh``.
1154

1155
        Returns a :class:`pwnlib.tubes.ssh.ssh_connecter` object.
1156

1157
        Examples:
1158

1159
            >>> from pwn import *
1160
            >>> l = listen()
1161
            >>> s =  ssh(host='example.pwnme')
1162
            >>> a = s.connect_remote(s.host, l.lport)
1163
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1164
            >>> a.sendline(b'Hello')
1165
            >>> print(repr(b.recvline()))
1166
            b'Hello\n'
1167
        """
1168

1169
        return ssh_connecter(self, host, port, timeout, level=self.level)
1✔
1170

1171
    remote = connect_remote
1✔
1172

1173
    def listen_remote(self, port = 0, bind_address = '', timeout = Timeout.default):
1✔
1174
        r"""listen_remote(port = 0, bind_address = '', timeout = Timeout.default) -> ssh_connecter
1175

1176
        Listens remotely through an SSH connection. This is equivalent to
1177
        using the ``-R`` flag on ``ssh``.
1178

1179
        Returns a :class:`pwnlib.tubes.ssh.ssh_listener` object.
1180

1181
        Examples:
1182

1183
            >>> from pwn import *
1184
            >>> s =  ssh(host='example.pwnme')
1185
            >>> l = s.listen_remote()
1186
            >>> a = remote(s.host, l.port)
1187
            >>> a=a; b = l.wait_for_connection()  # a=a; prevents hangs
1188
            >>> a.sendline(b'Hello')
1189
            >>> print(repr(b.recvline()))
1190
            b'Hello\n'
1191
        """
1192

1193
        return ssh_listener(self, bind_address, port, timeout, level=self.level)
1✔
1194

1195
    listen = listen_remote
1✔
1196

1197
    def __getitem__(self, attr):
1✔
1198
        """Permits indexed access to run commands over SSH
1199

1200
        Examples:
1201

1202
            >>> s =  ssh(host='example.pwnme')
1203
            >>> print(repr(s['echo hello']))
1204
            b'hello'
1205
        """
1206
        return self.run(attr).recvall().strip()
1✔
1207

1208
    def __call__(self, attr):
1✔
1209
        """Permits function-style access to run commands over SSH
1210

1211
        Examples:
1212

1213
            >>> s =  ssh(host='example.pwnme')
1214
            >>> print(repr(s('echo hello')))
1215
            b'hello'
1216
        """
1217
        return self.run(attr).recvall().strip()
1✔
1218

1219
    def __getattr__(self, attr):
1✔
1220
        """Permits member access to run commands over SSH
1221

1222
        Examples:
1223

1224
            >>> s =  ssh(host='example.pwnme')
1225
            >>> s.echo('hello')
1226
            b'hello'
1227
            >>> s.whoami()
1228
            b'travis'
1229
            >>> s.echo(['huh','yay','args'])
1230
            b'huh yay args'
1231
        """
1232
        bad_attrs = [
1✔
1233
            'trait_names',          # ipython tab-complete
1234
        ]
1235

1236
        if attr in self.__dict__ \
1!
1237
        or attr in bad_attrs \
1238
        or attr.startswith('_'):
1239
            raise AttributeError
×
1240

1241
        @LocalContext
1✔
1242
        def runner(*args):
1✔
1243
            if len(args) == 1 and isinstance(args[0], (list, tuple)):
1✔
1244
                command = [attr]
1✔
1245
                command.extend(args[0])
1✔
1246
            else:
1247
                command = [attr]
1✔
1248
                command.extend(args)
1✔
1249
                command = b' '.join(packing._need_bytes(arg, min_wrong=0x80) for arg in command)
1✔
1250

1251
            return self.run(command).recvall().strip()
1✔
1252
        return runner
1✔
1253

1254
    def connected(self):
1✔
1255
        """Returns True if we are connected.
1256

1257
        Example:
1258

1259
            >>> s =  ssh(host='example.pwnme')
1260
            >>> s.connected()
1261
            True
1262
            >>> s.close()
1263
            >>> s.connected()
1264
            False
1265
        """
1266
        return bool(self.client and self.client.get_transport().is_active())
1✔
1267

1268
    def close(self):
1✔
1269
        """Close the connection."""
1270
        if self.client:
1!
1271
            self.client.close()
1✔
1272
            self.client = None
1✔
1273
            self.info("Closed connection to %r" % self.host)
1✔
1274

1275
    def _libs_remote(self, remote):
1✔
1276
        """Return a dictionary of the libraries used by a remote file."""
1277
        escaped_remote = sh_string(remote)
1✔
1278
        cmd = ''.join([
1✔
1279
            '(',
1280
            'ulimit -s unlimited;',
1281
            'ldd %s > /dev/null &&' % escaped_remote,
1282
            '(',
1283
            'LD_TRACE_LOADED_OBJECTS=1 %s||' % escaped_remote,
1284
            'ldd %s' % escaped_remote,
1285
            '))',
1286
            ' 2>/dev/null'
1287
        ])
1288
        data, status = self.run_to_end(cmd)
1✔
1289
        if status != 0:
1!
1290
            self.error('Unable to find libraries for %r' % remote)
×
1291
            return {}
×
1292

1293
        return misc.parse_ldd_output(packing._decode(data))
1✔
1294

1295
    def _get_fingerprint(self, remote):
1✔
1296
        cmd = '(sha256 || sha256sum || openssl sha256) 2>/dev/null < '
1✔
1297
        cmd = cmd + sh_string(remote)
1✔
1298

1299
        data, status = self.run_to_end(cmd)
1✔
1300

1301
        if status != 0:
1!
1302
            return None
×
1303

1304
        if not isinstance(data, str):
1✔
1305
            data = data.decode('ascii')
1✔
1306

1307
        data = re.search("([a-fA-F0-9]{64})",data).group()
1✔
1308
        return data
1✔
1309

1310
    def _get_cachefile(self, fingerprint):
1✔
1311
        return os.path.join(self._cachedir, fingerprint)
1✔
1312

1313
    def _verify_local_fingerprint(self, fingerprint):
1✔
1314
        if not set(fingerprint).issubset(string.hexdigits) or \
1!
1315
           len(fingerprint) != 64:
1316
            self.error('Invalid fingerprint %r' % fingerprint)
×
1317
            return False
×
1318

1319
        local = self._get_cachefile(fingerprint)
1✔
1320
        if not os.path.isfile(local):
1✔
1321
            return False
1✔
1322

1323
        if hashes.sha256filehex(local) == fingerprint:
1!
1324
            return True
1✔
1325
        else:
1326
            os.unlink(local)
×
1327
            return False
×
1328

1329
    def _download_raw(self, remote, local, h):
1✔
1330
        def update(has, total):
1✔
1331
            h.status("%s/%s" % (misc.size(has), misc.size(total)))
1✔
1332

1333
        if self.sftp:
1✔
1334
            try:
1✔
1335
                self.sftp.get(remote, local, update)
1✔
1336
                return
1✔
1337
            except IOError:
×
1338
                pass
×
1339

1340
        cmd = 'wc -c < ' + sh_string(remote)
1✔
1341
        total, exitcode = self.run_to_end(cmd)
1✔
1342

1343
        if exitcode != 0:
1!
1344
            h.failure("%r does not exist or is not accessible" % remote)
×
1345
            return
×
1346

1347
        total = int(total)
1✔
1348

1349
        with context.local(log_level = 'ERROR'):
1✔
1350
            cmd = 'cat < ' + sh_string(remote)
1✔
1351
            c = self.run(cmd)
1✔
1352
        data = b''
1✔
1353

1354
        while True:
1✔
1355
            try:
1✔
1356
                data += c.recv()
1✔
1357
            except EOFError:
1✔
1358
                break
1✔
1359
            update(len(data), total)
1✔
1360

1361
        result = c.wait()
1✔
1362
        if result != 0:
1!
1363
            h.failure('Could not download file %r (%r)' % (remote, result))
×
1364
            return
×
1365

1366
        with open(local, 'wb') as fd:
1✔
1367
            fd.write(data)
1✔
1368

1369
    def _download_to_cache(self, remote, p, fingerprint=True):
1✔
1370

1371
        with context.local(log_level='error'):
1✔
1372
            remote = self.readlink('-f',remote)
1✔
1373
        if not hasattr(remote, 'encode'):
1✔
1374
            remote = remote.decode('utf-8')
1✔
1375

1376
        fingerprint = fingerprint and self._get_fingerprint(remote) or None
1✔
1377
        if fingerprint is None:
1✔
1378
            local = os.path.normpath(remote)
1✔
1379
            local = os.path.basename(local)
1✔
1380
            local += time.strftime('-%Y-%m-%d-%H:%M:%S')
1✔
1381
            local = os.path.join(self._cachedir, local)
1✔
1382

1383
            self._download_raw(remote, local, p)
1✔
1384
            return local
1✔
1385

1386
        local = self._get_cachefile(fingerprint)
1✔
1387

1388
        if self.cache and self._verify_local_fingerprint(fingerprint):
1✔
1389
            p.success('Found %r in ssh cache' % remote)
1✔
1390
        else:
1391
            self._download_raw(remote, local, p)
1✔
1392

1393
            if not self._verify_local_fingerprint(fingerprint):
1!
1394
                self.error('Could not download file %r', remote)
×
1395

1396
        return local
1✔
1397

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

1401
        Arguments:
1402
            remote(str): The remote filename to download.
1403

1404

1405
        Examples:
1406

1407
            >>> with open('/tmp/bar','w+') as f:
1408
            ...     _ = f.write('Hello, world')
1409
            >>> s =  ssh(host='example.pwnme',
1410
            ...         cache=False)
1411
            >>> s.download_data('/tmp/bar')
1412
            b'Hello, world'
1413
            >>> s._sftp = None
1414
            >>> s._tried_sftp = True
1415
            >>> s.download_data('/tmp/bar')
1416
            b'Hello, world'
1417

1418
        """
1419
        with self.progress('Downloading %r' % remote) as p:
1✔
1420
            with open(self._download_to_cache(remote, p, fingerprint), 'rb') as fd:
1✔
1421
                return fd.read()
1✔
1422

1423
    def download_file(self, remote, local = None):
1✔
1424
        """Downloads a file from the remote server.
1425

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

1429
        Arguments:
1430
            remote(str/bytes): The remote filename to download
1431
            local(str): The local filename to save it to. Default is to infer it from the remote filename.
1432
        
1433
        Examples:
1434

1435
            >>> with open('/tmp/foobar','w+') as f:
1436
            ...     _ = f.write('Hello, world')
1437
            >>> s =  ssh(host='example.pwnme',
1438
            ...         cache=False)
1439
            >>> _ = s.set_working_directory(wd='/tmp')
1440
            >>> _ = s.download_file('foobar', 'barfoo')
1441
            >>> with open('barfoo','r') as f:
1442
            ...     print(f.read())
1443
            Hello, world
1444
        """
1445

1446

1447
        if not local:
1✔
1448
            local = os.path.basename(os.path.normpath(remote))
1✔
1449

1450
        with self.progress('Downloading %r to %r' % (remote, local)) as p:
1✔
1451
            local_tmp = self._download_to_cache(remote, p)
1✔
1452

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

1457
    def download_dir(self, remote=None, local=None, ignore_failed_read=False):
1✔
1458
        """Recursively downloads a directory from the remote server
1459

1460
        Arguments:
1461
            local: Local directory
1462
            remote: Remote directory
1463
        """
1464
        remote = packing._encode(remote or self.cwd)
×
1465

1466
        if self.sftp:
×
1467
            remote = packing._encode(self.sftp.normalize(remote))
×
1468
        else:
1469
            with context.local(log_level='error'):
×
1470
                remote = self.system(b'readlink -f ' + sh_string(remote)).recvall().strip()
×
1471

1472
        local = local or '.'
×
1473
        local = os.path.expanduser(local)
×
1474

1475
        self.info("Downloading %r to %r" % (remote, local))
×
1476

1477
        if ignore_failed_read:
×
1478
            opts = b" --ignore-failed-read"
×
1479
        else:
1480
            opts = b""
×
1481
        with context.local(log_level='error'):
×
1482
            remote_tar = self.mktemp()
×
1483
            cmd = b'tar %s -C %s -czf %s .' % \
×
1484
                  (opts,
1485
                   sh_string(remote),
1486
                   sh_string(remote_tar))
1487
            tar = self.system(cmd)
×
1488

1489
            if 0 != tar.wait():
×
1490
                self.error("Could not create remote tar")
×
1491

1492
            local_tar = tempfile.NamedTemporaryFile(suffix='.tar.gz')
×
1493
            self.download_file(remote_tar, local_tar.name)
×
1494

1495
            # Delete temporary tarfile from remote host
1496
            if self.sftp:
×
1497
                self.unlink(remote_tar)
×
1498
            else:
1499
                self.system(b'rm ' + sh_string(remote_tar)).wait()
×
1500
            tar = tarfile.open(local_tar.name)
×
1501
            tar.extractall(local)
×
1502

1503

1504
    def upload_data(self, data, remote):
1✔
1505
        """Uploads some data into a file on the remote server.
1506

1507
        Arguments:
1508
            data(str): The data to upload.
1509
            remote(str): The filename to upload it to.
1510

1511
        Example:
1512

1513
            >>> s =  ssh(host='example.pwnme')
1514
            >>> s.upload_data(b'Hello, world', '/tmp/upload_foo')
1515
            >>> print(open('/tmp/upload_foo').read())
1516
            Hello, world
1517
            >>> s._sftp = False
1518
            >>> s._tried_sftp = True
1519
            >>> s.upload_data(b'Hello, world', '/tmp/upload_bar')
1520
            >>> print(open('/tmp/upload_bar').read())
1521
            Hello, world
1522
        """
1523
        data = packing._need_bytes(data)
1✔
1524
        # If a relative path was provided, prepend the cwd
1525
        if os.path.normpath(remote) == os.path.basename(remote):
1✔
1526
            remote = os.path.join(self.cwd, remote)
1✔
1527

1528
        if self.sftp:
1✔
1529
            with tempfile.NamedTemporaryFile() as f:
1✔
1530
                f.write(data)
1✔
1531
                f.flush()
1✔
1532
                self.sftp.put(f.name, remote)
1✔
1533
                return
1✔
1534

1535
        with context.local(log_level = 'ERROR'):
1✔
1536
            cmd = 'cat > ' + sh_string(remote)
1✔
1537
            s = self.run(cmd, tty=False)
1✔
1538
            s.send(data)
1✔
1539
            s.shutdown('send')
1✔
1540
            data   = s.recvall()
1✔
1541
            result = s.wait()
1✔
1542
            if result != 0:
1!
1543
                self.error("Could not upload file %r (%r)\n%s" % (remote, result, data))
×
1544

1545
    def upload_file(self, filename, remote = None):
1✔
1546
        """Uploads a file to the remote server. Returns the remote filename.
1547

1548
        Arguments:
1549
        filename(str): The local filename to download
1550
        remote(str): The remote filename to save it to. Default is to infer it from the local filename."""
1551

1552

1553
        if remote is None:
1✔
1554
            remote = os.path.normpath(filename)
1✔
1555
            remote = os.path.basename(remote)
1✔
1556
            remote = os.path.join(self.cwd, remote)
1✔
1557

1558
        with open(filename, 'rb') as fd:
1✔
1559
            data = fd.read()
1✔
1560

1561
        self.info("Uploading %r to %r" % (filename,remote))
1✔
1562
        self.upload_data(data, remote)
1✔
1563

1564
        return remote
1✔
1565

1566
    def upload_dir(self, local, remote=None):
1✔
1567
        """Recursively uploads a directory onto the remote server
1568

1569
        Arguments:
1570
            local: Local directory
1571
            remote: Remote directory
1572
        """
1573

1574
        remote    = packing._encode(remote or self.cwd)
×
1575

1576
        local     = os.path.expanduser(local)
×
1577
        dirname   = os.path.dirname(local)
×
1578
        basename  = os.path.basename(local)
×
1579

1580
        if not os.path.isdir(local):
×
1581
            self.error("%r is not a directory" % local)
×
1582

1583
        msg = "Uploading %r to %r" % (basename,remote)
×
1584
        with self.waitfor(msg):
×
1585
            # Generate a tarfile with everything inside of it
1586
            local_tar  = tempfile.mktemp()
×
1587
            with tarfile.open(local_tar, 'w:gz') as tar:
×
1588
                tar.add(local, basename)
×
1589

1590
            # Upload and extract it
1591
            with context.local(log_level='error'):
×
1592
                remote_tar = self.mktemp('--suffix=.tar.gz')
×
1593
                self.upload_file(local_tar, remote_tar)
×
1594

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

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

1601
    def upload(self, file_or_directory, remote=None):
1✔
1602
        """upload(file_or_directory, remote=None)
1603

1604
        Upload a file or directory to the remote host.
1605

1606
        Arguments:
1607
            file_or_directory(str): Path to the file or directory to download.
1608
            remote(str): Local path to store the data.
1609
                By default, uses the working directory.
1610
        """
1611
        if isinstance(file_or_directory, str):
1!
1612
            file_or_directory = os.path.expanduser(file_or_directory)
1✔
1613
            file_or_directory = os.path.expandvars(file_or_directory)
1✔
1614

1615
        if os.path.isfile(file_or_directory):
1!
1616
            return self.upload_file(file_or_directory, remote)
1✔
1617

1618
        if os.path.isdir(file_or_directory):
×
1619
            return self.upload_dir(file_or_directory, remote)
×
1620

1621
        self.error('%r does not exist' % file_or_directory)
×
1622

1623
    def download(self, file_or_directory, local=None):
1✔
1624
        """download(file_or_directory, local=None)
1625

1626
        Download a file or directory from the remote host.
1627

1628
        Arguments:
1629
            file_or_directory(str): Path to the file or directory to download.
1630
            local(str): Local path to store the data.
1631
                By default, uses the current directory.
1632
        
1633

1634
        Examples:
1635

1636
            >>> with open('/tmp/foobar','w+') as f:
1637
            ...     _ = f.write('Hello, world')
1638
            >>> s =  ssh(host='example.pwnme',
1639
            ...         cache=False)
1640
            >>> _ = s.set_working_directory('/tmp')
1641
            >>> _ = s.download('foobar', 'barfoo')
1642
            >>> with open('barfoo','r') as f:
1643
            ...     print(f.read())
1644
            Hello, world
1645
        """
1646
        file_or_directory = packing._encode(file_or_directory)
1✔
1647
        with self.system(b'test -d ' + sh_string(file_or_directory)) as io:
1✔
1648
            is_dir = io.wait()
1✔
1649

1650
        if 0 == is_dir:
1!
1651
            self.download_dir(file_or_directory, local)
×
1652
        else:
1653
            self.download_file(file_or_directory, local)
1✔
1654

1655
    put = upload
1✔
1656
    get = download
1✔
1657

1658
    def unlink(self, file):
1✔
1659
        """unlink(file)
1660

1661
        Delete the file on the remote host
1662

1663
        Arguments:
1664
            file(str): Path to the file
1665
        """
1666
        if not self.sftp:
×
1667
            self.error("unlink() is only supported if SFTP is supported")
×
1668

1669
        return self.sftp.unlink(file)
×
1670

1671
    def libs(self, remote, directory = None, flatten = False):
1✔
1672
        """Downloads the libraries referred to by a file.
1673

1674
        This is done by running ldd on the remote server, parsing the output
1675
        and downloading the relevant files.
1676

1677
        The directory argument specified where to download the files. This defaults
1678
        to './$HOSTNAME' where $HOSTNAME is the hostname of the remote server.
1679

1680
        Arguments:
1681
            remote(str): Remote file path
1682
            directory(str): Output directory
1683
            flatten(bool): Flatten the file tree if True (defaults to False) and
1684
                ignore the remote directory structure. If there are duplicate
1685
                filenames, an error will be raised.
1686
        """
1687

1688
        libs = self._libs_remote(remote)
1✔
1689

1690
        remote = packing._decode(self.readlink('-f',remote).strip())
1✔
1691
        libs[remote] = 0
1✔
1692

1693
        if flatten:
1!
1694
            basenames = dict()
×
1695

1696
            # If there is a duplicate switch to unflattened download
1697
            for lib in libs:
×
1698
                name = os.path.basename(lib)
×
1699

1700
                if name in basenames.values():
×
1701
                    duplicate = [key for key, value in basenames.items() if
×
1702
                                 value == name][0]
1703
                    self.error('Duplicate lib name: %r / %4r' % (lib, duplicate))
×
1704

1705
                basenames[lib] = name
×
1706

1707
        if directory is None:
1!
1708
            directory = self.host
1✔
1709

1710
        directory = os.path.realpath(directory)
1✔
1711

1712
        res = {}
1✔
1713

1714
        seen = set()
1✔
1715

1716
        for lib, addr in libs.items():
1✔
1717
            local = os.path.realpath(os.path.join(directory, '.' + os.path.sep \
1✔
1718
                    + (basenames[lib] if flatten else lib)))
1719
            if not local.startswith(directory):
1!
1720
                self.warning('This seems fishy: %r' % lib)
×
1721
                continue
×
1722

1723
            misc.mkdir_p(os.path.dirname(local))
1✔
1724

1725
            if lib not in seen:
1!
1726
                self.download_file(lib, local)
1✔
1727
                seen.add(lib)
1✔
1728
            res[local] = addr
1✔
1729

1730
        return res
1✔
1731

1732
    def interactive(self, shell=None):
1✔
1733
        """Create an interactive session.
1734

1735
        This is a simple wrapper for creating a new
1736
        :class:`pwnlib.tubes.ssh.ssh_channel` object and calling
1737
        :meth:`pwnlib.tubes.ssh.ssh_channel.interactive` on it."""
1738

1739
        s = self.shell(shell)
×
1740

1741
        if self.cwd != '.':
×
1742
            cmd = 'cd ' + sh_string(self.cwd)
×
1743
            s.sendline(packing._need_bytes(cmd, 2, 0x80))
×
1744

1745
        s.interactive()
×
1746
        s.close()
×
1747

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

1753
        Note:
1754
            This uses ``mktemp -d`` under the covers, sets permissions
1755
            on the directory to ``0700``.  This means that setuid binaries
1756
            will **not** be able to access files created in this directory.
1757

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

1760
        Arguments:
1761
            wd(string): Working directory.  Default is to auto-generate a directory
1762
                based on the result of running 'mktemp -d' on the remote machine.
1763
            symlink(bool,str): Create symlinks in the new directory.
1764

1765
                The default value, ``False``, implies that no symlinks should be
1766
                created.
1767

1768
                A string value is treated as a path that should be symlinked.
1769
                It is passed directly to the shell on the remote end for expansion,
1770
                so wildcards work.
1771

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

1775
        Examples:
1776

1777
            >>> s =  ssh(host='example.pwnme')
1778
            >>> cwd = s.set_working_directory()
1779
            >>> s.ls()
1780
            b''
1781
            >>> packing._decode(s.pwd()) == cwd
1782
            True
1783

1784
            >>> s =  ssh(host='example.pwnme')
1785
            >>> homedir = s.pwd()
1786
            >>> _=s.touch('foo')
1787

1788
            >>> _=s.set_working_directory()
1789
            >>> assert s.ls() == b''
1790

1791
            >>> _=s.set_working_directory(homedir)
1792
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1793

1794
            >>> _=s.set_working_directory(symlink=True)
1795
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1796
            >>> assert homedir != s.pwd()
1797

1798
            >>> symlink=os.path.join(homedir,b'*')
1799
            >>> _=s.set_working_directory(symlink=symlink)
1800
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1801
            >>> assert homedir != s.pwd()
1802

1803
            >>> _=s.set_working_directory()
1804
            >>> io = s.system('pwd')
1805
            >>> io.recvallS().strip() == io.cwd
1806
            True
1807
            >>> io.cwd == s.cwd
1808
            True
1809
        """
1810
        status = 0
1✔
1811

1812
        if symlink and not isinstance(symlink, (six.binary_type, six.text_type)):
1✔
1813
            symlink = os.path.join(self.pwd(), b'*')
1✔
1814
        if not hasattr(symlink, 'encode') and hasattr(symlink, 'decode'):
1✔
1815
            symlink = symlink.decode('utf-8')
1✔
1816
            
1817
        if isinstance(wd, six.text_type):
1✔
1818
            wd = packing._need_bytes(wd, 2, 0x80)
1✔
1819

1820
        if not wd:
1✔
1821
            wd, status = self.run_to_end('x=$(mktemp -d) && cd $x && chmod +x . && echo $PWD', cwd='.')
1✔
1822
            wd = wd.strip()
1✔
1823

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

1827
        else:
1828
            cmd = b'ls ' + sh_string(wd)
1✔
1829
            _, status = self.run_to_end(cmd, wd = '.')
1✔
1830

1831
            if status:
1!
1832
                self.error("%r does not appear to exist" % wd)
×
1833

1834
        if not isinstance(wd, str):
1✔
1835
            wd = wd.decode('utf-8')
1✔
1836
        self.cwd = wd
1✔
1837

1838
        self.info("Working directory: %r" % self.cwd)
1✔
1839

1840
        if symlink:
1✔
1841
            self.ln('-s', symlink, '.')
1✔
1842

1843
        return wd
1✔
1844

1845
    def write(self, path, data):
1✔
1846
        """Wrapper around upload_data to match :func:`pwnlib.util.misc.write`"""
1847
        data = packing._need_bytes(data)
1✔
1848
        return self.upload_data(data, path)
1✔
1849

1850
    def read(self, path):
1✔
1851
        """Wrapper around download_data to match :func:`pwnlib.util.misc.read`"""
1852
        return self.download_data(path)
1✔
1853

1854
    def _init_remote_platform_info(self):
1✔
1855
        r"""Fills _platform_info, e.g.:
1856

1857
        ::
1858

1859
            {'distro': 'Ubuntu\n',
1860
             'distro_ver': '14.04\n',
1861
             'machine': 'x86_64',
1862
             'node': 'pwnable.kr',
1863
             'processor': 'x86_64',
1864
             'release': '3.11.0-12-generic',
1865
             'system': 'linux',
1866
             'version': '#19-ubuntu smp wed oct 9 16:20:46 utc 2013'}
1867
        """
1868
        if self._platform_info:
1✔
1869
            return
1✔
1870

1871
        def preexec():
1✔
1872
            import platform
×
1873
            print('\n'.join(platform.uname()))
×
1874

1875
        with context.quiet:
1✔
1876
            with self.process('true', preexec_fn=preexec) as io:
1✔
1877

1878
                self._platform_info = {
1✔
1879
                    'system': io.recvline().lower().strip().decode(),
1880
                    'node': io.recvline().lower().strip().decode(),
1881
                    'release': io.recvline().lower().strip().decode(),
1882
                    'version': io.recvline().lower().strip().decode(),
1883
                    'machine': io.recvline().lower().strip().decode(),
1884
                    'processor': io.recvline().lower().strip().decode(),
1885
                    'distro': 'Unknown',
1886
                    'distro_ver': ''
1887
                }
1888

1889
            try:
1✔
1890
                if not self.which('lsb_release'):
1!
1891
                    return
×
1892

1893
                with self.process(['lsb_release', '-irs']) as io:
1✔
1894
                    lsb_info = io.recvall().strip().decode()
1✔
1895
                    self._platform_info['distro'], self._platform_info['distro_ver'] = lsb_info.split()
1✔
1896
            except Exception:
×
1897
                pass
×
1898

1899
    @property
1✔
1900
    def os(self):
1✔
1901
        """:class:`str`: Operating System of the remote machine."""
1902
        try:
1✔
1903
            self._init_remote_platform_info()
1✔
1904
            with context.local(os=self._platform_info['system']):
1✔
1905
                return context.os
1✔
1906
        except Exception:
×
1907
            return "Unknown"
×
1908

1909

1910
    @property
1✔
1911
    def arch(self):
1✔
1912
        """:class:`str`: CPU Architecture of the remote machine."""
1913
        try:
1✔
1914
            self._init_remote_platform_info()
1✔
1915
            with context.local(arch=self._platform_info['machine']):
1✔
1916
                return context.arch
1✔
1917
        except Exception:
×
1918
            return "Unknown"
×
1919

1920
    @property
1✔
1921
    def bits(self):
1✔
1922
        """:class:`str`: Pointer size of the remote machine."""
1923
        try:
×
1924
            with context.local():
×
1925
                context.clear()
×
1926
                context.arch = self.arch
×
1927
                return context.bits
×
1928
        except Exception:
×
1929
            return context.bits
×
1930

1931
    @property
1✔
1932
    def version(self):
1✔
1933
        """:class:`tuple`: Kernel version of the remote machine."""
1934
        try:
1✔
1935
            self._init_remote_platform_info()
1✔
1936
            vers = self._platform_info['release']
1✔
1937

1938
            # 3.11.0-12-generic
1939
            expr = r'([0-9]+\.?)+'
1✔
1940

1941
            vers = re.search(expr, vers).group()
1✔
1942
            return tuple(map(int, vers.split('.')))
1✔
1943

1944
        except Exception:
×
1945
            return (0,0,0)
×
1946

1947
    @property
1✔
1948
    def distro(self):
1✔
1949
        """:class:`tuple`: Linux distribution name and release."""
1950
        try:
1✔
1951
            self._init_remote_platform_info()
1✔
1952
            return (self._platform_info['distro'], self._platform_info['distro_ver'])
1✔
1953
        except Exception:
×
1954
            return ("Unknown", "Unknown")
×
1955

1956
    @property
1✔
1957
    def aslr(self):
1✔
1958
        """:class:`bool`: Whether ASLR is enabled on the system.
1959

1960
        Example:
1961

1962
            >>> s = ssh("travis", "example.pwnme")
1963
            >>> s.aslr
1964
            True
1965
        """
1966
        if self._aslr is None:
1!
1967
            if self.os != 'linux':
1!
1968
                self.warn_once("Only Linux is supported for ASLR checks.")
×
1969
                self._aslr = False
×
1970

1971
            else:
1972
                with context.quiet:
1✔
1973
                    rvs = self.read('/proc/sys/kernel/randomize_va_space')
1✔
1974

1975
                self._aslr = not rvs.startswith(b'0')
1✔
1976

1977
        return self._aslr
1✔
1978

1979
    @property
1✔
1980
    def aslr_ulimit(self):
1✔
1981
        """:class:`bool`: Whether the entropy of 32-bit processes can be reduced with ulimit."""
1982
        import pwnlib.elf.elf
1✔
1983
        import pwnlib.shellcraft
1✔
1984

1985
        if self._aslr_ulimit is not None:
1!
1986
            return self._aslr_ulimit
×
1987

1988
        # This test must run a 32-bit binary, fix the architecture
1989
        arch = {
1✔
1990
            'amd64': 'i386',
1991
            'aarch64': 'arm'
1992
        }.get(self.arch, self.arch)
1993

1994
        with context.local(arch=arch, bits=32, os=self.os, aslr=True):
1!
1995
            with context.quiet:
1✔
1996
                try:
1✔
1997
                    sc = pwnlib.shellcraft.cat('/proc/self/maps') \
1✔
1998
                       + pwnlib.shellcraft.exit(0)
1999

2000
                    elf = pwnlib.elf.elf.ELF.from_assembly(sc, shared=True)
1✔
2001
                except Exception:
×
2002
                    self.warn_once("Can't determine ulimit ASLR status")
×
2003
                    self._aslr_ulimit = False
×
2004
                    return self._aslr_ulimit
×
2005

2006
                def preexec():
1✔
2007
                    import resource
×
2008
                    try:
×
2009
                        resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
2010
                    except Exception:
×
2011
                        pass
×
2012

2013
                # Move to a new temporary directory
2014
                cwd = self.cwd
1✔
2015
                tmp = self.set_working_directory()
1✔
2016

2017
                try:
1✔
2018
                    self.upload(elf.path, './aslr-test')
1✔
2019
                except IOError:
×
2020
                    self.warn_once("Couldn't check ASLR ulimit trick")
×
2021
                    self._aslr_ulimit = False
×
2022
                    return False
×
2023

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

2027
                # Move back to the old directory
2028
                self.cwd = cwd
1✔
2029

2030
                # Clean up the files
2031
                self.process(['rm', '-rf', tmp]).wait()
1✔
2032

2033
        # Check for 555555000 (1/3 of the address space for PAE)
2034
        # and for 40000000 (1/3 of the address space with 3BG barrier)
2035
        self._aslr_ulimit = bool(b'55555000' in maps or b'40000000' in maps)
1✔
2036

2037
        return self._aslr_ulimit
1✔
2038

2039
    def _cpuinfo(self):
1✔
2040
        if self._cpuinfo_cache is None:
1✔
2041
            with context.quiet:
1✔
2042
                try:
1✔
2043
                    self._cpuinfo_cache = self.download_data('/proc/cpuinfo', fingerprint=False)
1✔
2044
                except PwnlibException:
×
2045
                    self._cpuinfo_cache = b''
×
2046
        return self._cpuinfo_cache
1✔
2047

2048
    @property
1✔
2049
    def user_shstk(self):
1✔
2050
        """:class:`bool`: Whether userspace shadow stack is supported on the system.
2051

2052
        Example:
2053

2054
            >>> s = ssh("travis", "example.pwnme")
2055
            >>> s.user_shstk
2056
            False
2057
        """
2058
        if self._user_shstk is None:
1!
2059
            if self.os != 'linux':
1!
2060
                self.warn_once("Only Linux is supported for userspace shadow stack checks.")
×
2061
                self._user_shstk = False
×
2062

2063
            else:
2064
                cpuinfo = self._cpuinfo()
1✔
2065

2066
                self._user_shstk = b' user_shstk' in cpuinfo
1✔
2067
        return self._user_shstk
1✔
2068

2069
    @property
1✔
2070
    def ibt(self):
1✔
2071
        """:class:`bool`: Whether kernel indirect branch tracking is supported on the system.
2072

2073
        Example:
2074

2075
            >>> s = ssh("travis", "example.pwnme")
2076
            >>> s.ibt
2077
            False
2078
        """
2079
        if self._ibt is None:
1!
2080
            if self.os != 'linux':
1!
2081
                self.warn_once("Only Linux is supported for kernel indirect branch tracking checks.")
×
2082
                self._ibt = False
×
2083

2084
            else:
2085
                cpuinfo = self._cpuinfo()
1✔
2086

2087
                self._ibt = b' ibt ' in cpuinfo or b' ibt\n' in cpuinfo
1✔
2088
        return self._ibt
1✔
2089

2090
    def _checksec_cache(self, value=None):
1✔
2091
        path = self._get_cachefile('%s-%s' % (self.host, self.port))
1✔
2092

2093
        if value is not None:
1✔
2094
            with open(path, 'w+') as f:
1✔
2095
                f.write(value)
1✔
2096
        elif os.path.exists(path):
1✔
2097
            with open(path, 'r+') as f:
1✔
2098
                return f.read()
1✔
2099

2100
    def checksec(self, banner=True):
1✔
2101
        """checksec()
2102

2103
        Prints a helpful message about the remote system.
2104

2105
        Arguments:
2106
            banner(bool): Whether to print the path to the ELF binary.
2107
        """
2108
        cached = self._checksec_cache()
1✔
2109
        if cached:
1✔
2110
            return cached
1✔
2111

2112
        red    = text.red
1✔
2113
        green  = text.green
1✔
2114
        yellow = text.yellow
1✔
2115

2116
        res = [
1✔
2117
            "%s@%s:" % (self.user, self.host),
2118
            "Distro".ljust(10) + ' '.join(self.distro),
2119
            "OS:".ljust(10) + self.os,
2120
            "Arch:".ljust(10) + self.arch,
2121
            "Version:".ljust(10) + '.'.join(map(str, self.version)),
2122

2123
            "ASLR:".ljust(10) + {
2124
                True: green("Enabled"),
2125
                False: red("Disabled")
2126
            }[self.aslr],
2127
            "SHSTK:".ljust(10) + {
2128
                True: green("Enabled"),
2129
                False: red("Disabled")
2130
            }[self.user_shstk],
2131
            "IBT:".ljust(10) + {
2132
                True: green("Enabled"),
2133
                False: red("Disabled")
2134
            }[self.ibt],
2135
        ]
2136

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

2140
        cached = '\n'.join(res)
1✔
2141
        self._checksec_cache(cached)
1✔
2142
        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