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

Gallopsled / pwntools / 13600950642

01 Mar 2025 04:10AM UTC coverage: 74.211% (+3.2%) from 71.055%
13600950642

Pull #2546

github

web-flow
Merge 77df40314 into 60cff2437
Pull Request #2546: ssh: Allow passing `disabled_algorithms` keyword argument from `ssh` to paramiko

3812 of 6380 branches covered (59.75%)

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

1243 existing lines in 37 files now uncovered.

13352 of 17992 relevant lines covered (74.21%)

0.74 hits per line

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

70.15
/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 string
1✔
9
import sys
1✔
10
import tarfile
1✔
11
import tempfile
1✔
12
import threading
1✔
13
import time
1✔
14

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

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

36
class ssh_channel(sock):
1✔
37

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

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

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

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

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

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

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

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

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

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

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

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

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

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

98

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

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

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

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

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

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

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

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

139
            h.success()
1✔
140

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

144
        Kills the process.
145
        """
146

UNCOV
147
        self.close()
×
148

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

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

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

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

170
        return data
1✔
171

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

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

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

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

189
        return self.returncode
1✔
190

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

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

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

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

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

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

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

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

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

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

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

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

277
        # Restore
UNCOV
278
        term.term.hide_cursor()
×
279

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

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

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

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

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

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

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

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

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

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

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

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

332

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

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

341
        Examples:
342

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

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

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

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

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

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

371

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

UNCOV
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

UNCOV
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

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

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

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

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

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

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

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

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

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

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

435

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

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

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

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

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

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

468
            h.success()
1✔
469

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

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

476

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

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

484
        self.host = parent.host
1✔
485

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

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

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

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

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

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

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

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

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

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

537

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

602
        Example proxying:
603

604
        .. doctest::
605
           :skipif: True
606

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

615
        Logger.__init__(self)
1✔
616
        if level is not None:
1!
UNCOV
617
            self.setLevel(level)
×
618

619

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

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

638
        misc.mkdir_p(self._cachedir)
1✔
639

640
        import paramiko
1✔
641

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

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

661
        keyfiles = [os.path.expanduser(keyfile)] if keyfile else []
1✔
662

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

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

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

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

UNCOV
681
                if proxy_command:
×
682
                    proxy_sock = paramiko.ProxyCommand(proxy_command)
×
683
            else:
684
                proxy_sock = None
1✔
685

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

699
            self.transport = self.client.get_transport()
1✔
700
            self.transport.use_compression(True)
1✔
701

702
            h.success()
1✔
703

704
        if self.raw:
1!
UNCOV
705
            return
×
706

707
        self._tried_sftp = False
1✔
708

709
        if self.sftp:
1!
710
            with context.quiet:
1✔
711
                self.cwd = packing._decode(self.pwd(tty=False))
1✔
712
        else:
UNCOV
713
            self.cwd = '.'
×
714

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

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

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

731
    @property
1✔
732
    def cwd(self):
1✔
733
        return self._cwd
1✔
734

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

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

752
        self._tried_sftp = True
1✔
753
        return self._sftp
1✔
754

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

760
    def __enter__(self, *a):
1✔
UNCOV
761
        return self
×
762

763
    def __exit__(self, *a, **kw):
1✔
UNCOV
764
        self.close()
×
765

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

769
        Open a new channel with a shell inside.
770

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

776
        Returns:
777
            Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
778

779
        Examples:
780

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

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

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

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

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

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

855
        Notes:
856
            Requires Python on the remote server.
857

858
        Examples:
859

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

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

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

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

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

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

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

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

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

941

942
        self.debug("Created execve script:\n" + script)
1✔
943

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

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

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

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

968
        if timeout == Timeout.default:
1✔
969
            timeout = self.timeout
1✔
970

971
        with self.progress(msg) as h:
1✔
972

973
            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✔
974
            with context.quiet:
1✔
975
                python = ssh_process(self, script, tty=True, cwd=cwd, raw=True, level=self.level, timeout=timeout)
1✔
976

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

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

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

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

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

1015
            h.success('pid %i' % python.pid)
1✔
1016

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

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

UNCOV
1025
            self.warn_once(message)
×
1026

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

1030
        return python
1✔
1031

1032
    def which(self, program):
1✔
1033
        """which(program) -> str
1034

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

1042
        program = packing._encode(program)
1✔
1043

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

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

1049
        return packing._decode(result)
1✔
1050

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

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

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

1060
        Return a :class:`pwnlib.tubes.ssh.ssh_channel` object.
1061

1062
        Examples:
1063

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

1086
        if timeout is None:
1✔
1087
            timeout = self.timeout
1✔
1088

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

1091
    #: Backward compatibility.  Use :meth:`system`
1092
    run = system
1✔
1093

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

1098
        Note:
1099

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

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

UNCOV
1112
            if not python:
×
1113
                self.error("Python is not installed on the remote system.")
×
1114

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

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

1123

1124

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

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

1132
        Examples:
1133

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

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

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

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

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

1157
        Returns a :class:`pwnlib.tubes.ssh.ssh_connecter` object.
1158

1159
        Examples:
1160

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

1171
        return ssh_connecter(self, host, port, timeout, level=self.level)
1✔
1172

1173
    remote = connect_remote
1✔
1174

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

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

1181
        Returns a :class:`pwnlib.tubes.ssh.ssh_listener` object.
1182

1183
        Examples:
1184

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

1195
        return ssh_listener(self, bind_address, port, timeout, level=self.level)
1✔
1196

1197
    listen = listen_remote
1✔
1198

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

1202
        Examples:
1203

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

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

1213
        Examples:
1214

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

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

1224
        Supports other keyword arguments which are passed to :meth:`.system`.
1225

1226
        Examples:
1227

1228
            >>> s =  ssh(host='example.pwnme')
1229
            >>> s.echo('hello')
1230
            b'hello'
1231
            >>> s.whoami()
1232
            b'travis'
1233
            >>> s.echo(['huh','yay','args'])
1234
            b'huh yay args'
1235
            >>> s.echo('value: $MYENV', env={'MYENV':'the env'})
1236
            b'value: the env'
1237
        """
1238
        bad_attrs = [
1✔
1239
            'trait_names',          # ipython tab-complete
1240
        ]
1241

1242
        if attr in self.__dict__ \
1!
1243
        or attr in bad_attrs \
1244
        or attr.startswith('_'):
UNCOV
1245
            raise AttributeError
×
1246

1247
        @LocalContext
1✔
1248
        def runner(*args, **kwargs):
1✔
1249
            if len(args) == 1 and isinstance(args[0], (list, tuple)):
1✔
1250
                command = [attr]
1✔
1251
                command.extend(args[0])
1✔
1252
            else:
1253
                command = [attr]
1✔
1254
                command.extend(args)
1✔
1255
                command = b' '.join(packing._need_bytes(arg, min_wrong=0x80) for arg in command)
1✔
1256

1257
            return self.system(command, **kwargs).recvall().strip()
1✔
1258
        return runner
1✔
1259

1260
    def connected(self):
1✔
1261
        """Returns True if we are connected.
1262

1263
        Example:
1264

1265
            >>> s =  ssh(host='example.pwnme')
1266
            >>> s.connected()
1267
            True
1268
            >>> s.close()
1269
            >>> s.connected()
1270
            False
1271
        """
1272
        return bool(self.client and self.client.get_transport().is_active())
1✔
1273

1274
    def close(self):
1✔
1275
        """Close the connection."""
1276
        if self.client:
1!
1277
            self.client.close()
1✔
1278
            self.client = None
1✔
1279
            self.info("Closed connection to %r" % self.host)
1✔
1280

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

1299
        return misc.parse_ldd_output(packing._decode(data))
1✔
1300

1301
    def _get_fingerprint(self, remote):
1✔
1302
        cmd = '(sha256 || sha256sum || openssl sha256) 2>/dev/null < '
1✔
1303
        cmd = cmd + sh_string(remote)
1✔
1304

1305
        data, status = self.run_to_end(cmd)
1✔
1306

1307
        if status != 0:
1!
UNCOV
1308
            return None
×
1309

1310
        if not isinstance(data, str):
1!
1311
            data = data.decode('ascii')
1✔
1312

1313
        data = re.search("([a-fA-F0-9]{64})",data).group()
1✔
1314
        return data
1✔
1315

1316
    def _get_cachefile(self, fingerprint):
1✔
1317
        return os.path.join(self._cachedir, fingerprint)
1✔
1318

1319
    def _verify_local_fingerprint(self, fingerprint):
1✔
1320
        if not set(fingerprint).issubset(string.hexdigits) or \
1!
1321
           len(fingerprint) != 64:
UNCOV
1322
            self.error('Invalid fingerprint %r' % fingerprint)
×
1323
            return False
×
1324

1325
        local = self._get_cachefile(fingerprint)
1✔
1326
        if not os.path.isfile(local):
1✔
1327
            return False
1✔
1328

1329
        if hashes.sha256filehex(local) == fingerprint:
1!
1330
            return True
1✔
1331
        else:
UNCOV
1332
            os.unlink(local)
×
1333
            return False
×
1334

1335
    def _download_raw(self, remote, local, h):
1✔
1336
        def update(has, total):
1✔
1337
            h.status("%s/%s" % (misc.size(has), misc.size(total)))
1✔
1338

1339
        if self.sftp:
1✔
1340
            try:
1✔
1341
                self.sftp.get(remote, local, update)
1✔
1342
                return
1✔
UNCOV
1343
            except IOError:
×
1344
                pass
×
1345

1346
        cmd = 'wc -c < ' + sh_string(remote)
1✔
1347
        total, exitcode = self.run_to_end(cmd)
1✔
1348

1349
        if exitcode != 0:
1!
UNCOV
1350
            h.failure("%r does not exist or is not accessible" % remote)
×
1351
            return
×
1352

1353
        total = int(total)
1✔
1354

1355
        with context.local(log_level = 'ERROR'):
1✔
1356
            cmd = 'cat < ' + sh_string(remote)
1✔
1357
            c = self.system(cmd)
1✔
1358
        data = b''
1✔
1359

1360
        while True:
1✔
1361
            try:
1✔
1362
                data += c.recv()
1✔
1363
            except EOFError:
1✔
1364
                break
1✔
1365
            update(len(data), total)
1✔
1366

1367
        result = c.wait()
1✔
1368
        if result != 0:
1!
UNCOV
1369
            h.failure('Could not download file %r (%r)' % (remote, result))
×
1370
            return
×
1371

1372
        with open(local, 'wb') as fd:
1✔
1373
            fd.write(data)
1✔
1374

1375
    def _download_to_cache(self, remote, p, fingerprint=True):
1✔
1376

1377
        with context.local(log_level='error'):
1✔
1378
            remote = self.readlink('-f', remote, tty=False)
1✔
1379
        if not hasattr(remote, 'encode'):
1!
1380
            remote = remote.decode('utf-8')
1✔
1381

1382
        fingerprint = fingerprint and self._get_fingerprint(remote) or None
1✔
1383
        if fingerprint is None:
1✔
1384
            local = os.path.normpath(remote)
1✔
1385
            local = os.path.basename(local)
1✔
1386
            local += time.strftime('-%Y-%m-%d-%H:%M:%S')
1✔
1387
            local = os.path.join(self._cachedir, local)
1✔
1388

1389
            self._download_raw(remote, local, p)
1✔
1390
            return local
1✔
1391

1392
        local = self._get_cachefile(fingerprint)
1✔
1393

1394
        if self.cache and self._verify_local_fingerprint(fingerprint):
1✔
1395
            p.success('Found %r in ssh cache' % remote)
1✔
1396
        else:
1397
            self._download_raw(remote, local, p)
1✔
1398

1399
            if not self._verify_local_fingerprint(fingerprint):
1!
UNCOV
1400
                self.error('Could not download file %r', remote)
×
1401

1402
        return local
1✔
1403

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

1407
        Arguments:
1408
            remote(str): The remote filename to download.
1409

1410

1411
        Examples:
1412

1413
            >>> with open('/tmp/bar','w+') as f:
1414
            ...     _ = f.write('Hello, world')
1415
            >>> s =  ssh(host='example.pwnme',
1416
            ...         cache=False)
1417
            >>> s.download_data('/tmp/bar')
1418
            b'Hello, world'
1419
            >>> s._sftp = None
1420
            >>> s._tried_sftp = True
1421
            >>> s.download_data('/tmp/bar')
1422
            b'Hello, world'
1423

1424
        """
1425
        with self.progress('Downloading %r' % remote) as p:
1✔
1426
            with open(self._download_to_cache(remote, p, fingerprint), 'rb') as fd:
1✔
1427
                return fd.read()
1✔
1428

1429
    def download_file(self, remote, local = None):
1✔
1430
        """Downloads a file from the remote server.
1431

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

1435
        Arguments:
1436
            remote(str/bytes): The remote filename to download
1437
            local(str): The local filename to save it to. Default is to infer it from the remote filename.
1438
        
1439
        Examples:
1440

1441
            >>> with open('/tmp/foobar','w+') as f:
1442
            ...     _ = f.write('Hello, world')
1443
            >>> s =  ssh(host='example.pwnme',
1444
            ...         cache=False)
1445
            >>> _ = s.set_working_directory(wd='/tmp')
1446
            >>> _ = s.download_file('foobar', 'barfoo')
1447
            >>> with open('barfoo','r') as f:
1448
            ...     print(f.read())
1449
            Hello, world
1450
        """
1451

1452

1453
        if not local:
1✔
1454
            local = os.path.basename(os.path.normpath(remote))
1✔
1455

1456
        with self.progress('Downloading %r to %r' % (remote, local)) as p:
1✔
1457
            local_tmp = self._download_to_cache(remote, p)
1✔
1458

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

1463
    def download_dir(self, remote=None, local=None, ignore_failed_read=False):
1✔
1464
        """Recursively downloads a directory from the remote server
1465

1466
        Arguments:
1467
            local: Local directory
1468
            remote: Remote directory
1469
        """
UNCOV
1470
        remote = packing._encode(remote or self.cwd)
×
1471

UNCOV
1472
        if self.sftp:
×
1473
            remote = packing._encode(self.sftp.normalize(remote))
×
1474
        else:
UNCOV
1475
            with context.local(log_level='error'):
×
1476
                remote = self.system(b'readlink -f ' + sh_string(remote)).recvall().strip()
×
1477

UNCOV
1478
        local = local or '.'
×
1479
        local = os.path.expanduser(local)
×
1480

UNCOV
1481
        self.info("Downloading %r to %r" % (remote, local))
×
1482

UNCOV
1483
        if ignore_failed_read:
×
1484
            opts = b" --ignore-failed-read"
×
1485
        else:
UNCOV
1486
            opts = b""
×
1487
        with context.local(log_level='error'):
×
1488
            remote_tar = self.mktemp()
×
1489
            cmd = b'tar %s -C %s -czf %s .' % \
×
1490
                  (opts,
1491
                   sh_string(remote),
1492
                   sh_string(remote_tar))
UNCOV
1493
            tar = self.system(cmd)
×
1494

UNCOV
1495
            if 0 != tar.wait():
×
1496
                self.error("Could not create remote tar")
×
1497

UNCOV
1498
            local_tar = tempfile.NamedTemporaryFile(suffix='.tar.gz')
×
1499
            self.download_file(remote_tar, local_tar.name)
×
1500

1501
            # Delete temporary tarfile from remote host
UNCOV
1502
            if self.sftp:
×
1503
                self.unlink(remote_tar)
×
1504
            else:
UNCOV
1505
                self.system(b'rm ' + sh_string(remote_tar)).wait()
×
1506
            tar = tarfile.open(local_tar.name)
×
1507
            tar.extractall(local)
×
1508

1509

1510
    def upload_data(self, data, remote):
1✔
1511
        """Uploads some data into a file on the remote server.
1512

1513
        Arguments:
1514
            data(str): The data to upload.
1515
            remote(str): The filename to upload it to.
1516

1517
        Example:
1518

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

1534
        if self.sftp:
1✔
1535
            with tempfile.NamedTemporaryFile() as f:
1✔
1536
                f.write(data)
1✔
1537
                f.flush()
1✔
1538
                self.sftp.put(f.name, remote)
1✔
1539
                return
1✔
1540

1541
        with context.local(log_level = 'ERROR'):
1✔
1542
            cmd = 'cat > ' + sh_string(remote)
1✔
1543
            s = self.system(cmd, tty=False)
1✔
1544
            s.send(data)
1✔
1545
            s.shutdown('send')
1✔
1546
            data   = s.recvall()
1✔
1547
            result = s.wait()
1✔
1548
            if result != 0:
1!
UNCOV
1549
                self.error("Could not upload file %r (%r)\n%s" % (remote, result, data))
×
1550

1551
    def upload_file(self, filename, remote = None):
1✔
1552
        """Uploads a file to the remote server. Returns the remote filename.
1553

1554
        Arguments:
1555
        filename(str): The local filename to download
1556
        remote(str): The remote filename to save it to. Default is to infer it from the local filename."""
1557

1558

1559
        if remote is None:
1✔
1560
            remote = os.path.normpath(filename)
1✔
1561
            remote = os.path.basename(remote)
1✔
1562
            remote = os.path.join(self.cwd, remote)
1✔
1563

1564
        with open(filename, 'rb') as fd:
1✔
1565
            data = fd.read()
1✔
1566

1567
        self.info("Uploading %r to %r" % (filename,remote))
1✔
1568
        self.upload_data(data, remote)
1✔
1569

1570
        return remote
1✔
1571

1572
    def upload_dir(self, local, remote=None):
1✔
1573
        """Recursively uploads a directory onto the remote server
1574

1575
        Arguments:
1576
            local: Local directory
1577
            remote: Remote directory
1578
        """
1579

UNCOV
1580
        remote    = packing._encode(remote or self.cwd)
×
1581

UNCOV
1582
        local     = os.path.expanduser(local)
×
1583
        dirname   = os.path.dirname(local)
×
1584
        basename  = os.path.basename(local)
×
1585

UNCOV
1586
        if not os.path.isdir(local):
×
1587
            self.error("%r is not a directory" % local)
×
1588

UNCOV
1589
        msg = "Uploading %r to %r" % (basename,remote)
×
1590
        with self.waitfor(msg):
×
1591
            # Generate a tarfile with everything inside of it
UNCOV
1592
            local_tar  = tempfile.mktemp()
×
1593
            with tarfile.open(local_tar, 'w:gz') as tar:
×
1594
                tar.add(local, basename)
×
1595

1596
            # Upload and extract it
UNCOV
1597
            with context.local(log_level='error'):
×
1598
                remote_tar = self.mktemp('--suffix=.tar.gz')
×
1599
                self.upload_file(local_tar, remote_tar)
×
1600

UNCOV
1601
                untar = self.system(b'cd %s && tar -xzf %s' % (sh_string(remote), sh_string(remote_tar)))
×
1602
                message = untar.recvrepeat(2)
×
1603

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

1607
    def upload(self, file_or_directory, remote=None):
1✔
1608
        """upload(file_or_directory, remote=None)
1609

1610
        Upload a file or directory to the remote host.
1611

1612
        Arguments:
1613
            file_or_directory(str): Path to the file or directory to download.
1614
            remote(str): Local path to store the data.
1615
                By default, uses the working directory.
1616
        """
1617
        if isinstance(file_or_directory, str):
1!
1618
            file_or_directory = os.path.expanduser(file_or_directory)
1✔
1619
            file_or_directory = os.path.expandvars(file_or_directory)
1✔
1620

1621
        if os.path.isfile(file_or_directory):
1!
1622
            return self.upload_file(file_or_directory, remote)
1✔
1623

UNCOV
1624
        if os.path.isdir(file_or_directory):
×
1625
            return self.upload_dir(file_or_directory, remote)
×
1626

UNCOV
1627
        self.error('%r does not exist' % file_or_directory)
×
1628

1629
    def download(self, file_or_directory, local=None):
1✔
1630
        """download(file_or_directory, local=None)
1631

1632
        Download a file or directory from the remote host.
1633

1634
        Arguments:
1635
            file_or_directory(str): Path to the file or directory to download.
1636
            local(str): Local path to store the data.
1637
                By default, uses the current directory.
1638
        
1639

1640
        Examples:
1641

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

1656
        if 0 == is_dir:
1!
UNCOV
1657
            self.download_dir(file_or_directory, local)
×
1658
        else:
1659
            self.download_file(file_or_directory, local)
1✔
1660

1661
    put = upload
1✔
1662
    get = download
1✔
1663

1664
    def unlink(self, file):
1✔
1665
        """unlink(file)
1666

1667
        Delete the file on the remote host
1668

1669
        Arguments:
1670
            file(str): Path to the file
1671
        """
UNCOV
1672
        if not self.sftp:
×
1673
            self.error("unlink() is only supported if SFTP is supported")
×
1674

UNCOV
1675
        return self.sftp.unlink(file)
×
1676

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

1680
        This is done by running ldd on the remote server, parsing the output
1681
        and downloading the relevant files.
1682

1683
        The directory argument specified where to download the files. This defaults
1684
        to './$HOSTNAME' where $HOSTNAME is the hostname of the remote server.
1685

1686
        Arguments:
1687
            remote(str): Remote file path
1688
            directory(str): Output directory
1689
            flatten(bool): Flatten the file tree if True (defaults to False) and
1690
                ignore the remote directory structure. If there are duplicate
1691
                filenames, an error will be raised.
1692
        """
1693

1694
        libs = self._libs_remote(remote)
1✔
1695

1696
        remote = packing._decode(self.readlink('-f',remote).strip())
1✔
1697
        libs[remote] = 0
1✔
1698

1699
        if flatten:
1!
UNCOV
1700
            basenames = dict()
×
1701

1702
            # If there is a duplicate switch to unflattened download
UNCOV
1703
            for lib in libs:
×
1704
                name = os.path.basename(lib)
×
1705

UNCOV
1706
                if name in basenames.values():
×
1707
                    duplicate = [key for key, value in basenames.items() if
×
1708
                                 value == name][0]
UNCOV
1709
                    self.error('Duplicate lib name: %r / %4r' % (lib, duplicate))
×
1710

UNCOV
1711
                basenames[lib] = name
×
1712

1713
        if directory is None:
1!
1714
            directory = self.host
1✔
1715

1716
        directory = os.path.realpath(directory)
1✔
1717

1718
        res = {}
1✔
1719

1720
        seen = set()
1✔
1721

1722
        for lib, addr in libs.items():
1✔
1723
            local = os.path.realpath(os.path.join(directory, '.' + os.path.sep \
1✔
1724
                    + (basenames[lib] if flatten else lib)))
1725
            if not local.startswith(directory):
1!
UNCOV
1726
                self.warning('This seems fishy: %r' % lib)
×
1727
                continue
×
1728

1729
            misc.mkdir_p(os.path.dirname(local))
1✔
1730

1731
            if lib not in seen:
1!
1732
                self.download_file(lib, local)
1✔
1733
                seen.add(lib)
1✔
1734
            res[local] = addr
1✔
1735

1736
        return res
1✔
1737

1738
    def interactive(self, shell=None):
1✔
1739
        """Create an interactive session.
1740

1741
        This is a simple wrapper for creating a new
1742
        :class:`pwnlib.tubes.ssh.ssh_channel` object and calling
1743
        :meth:`pwnlib.tubes.ssh.ssh_channel.interactive` on it."""
1744

UNCOV
1745
        s = self.shell(shell)
×
1746

UNCOV
1747
        if self.cwd != '.':
×
1748
            cmd = 'cd ' + sh_string(self.cwd)
×
1749
            s.sendline(packing._need_bytes(cmd, 2, 0x80))
×
1750

UNCOV
1751
        s.interactive()
×
1752
        s.close()
×
1753

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

1759
        Note:
1760
            This uses ``mktemp -d`` under the covers, sets permissions
1761
            on the directory to ``0700``.  This means that setuid binaries
1762
            will **not** be able to access files created in this directory.
1763

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

1766
        Arguments:
1767
            wd(string): Working directory.  Default is to auto-generate a directory
1768
                based on the result of running 'mktemp -d' on the remote machine.
1769
            symlink(bool,str): Create symlinks in the new directory.
1770

1771
                The default value, ``False``, implies that no symlinks should be
1772
                created.
1773

1774
                A string value is treated as a path that should be symlinked.
1775
                It is passed directly to the shell on the remote end for expansion,
1776
                so wildcards work.
1777

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

1781
        Examples:
1782

1783
            >>> s =  ssh(host='example.pwnme')
1784
            >>> cwd = s.set_working_directory()
1785
            >>> s.ls()
1786
            b''
1787
            >>> packing._decode(s.pwd()) == cwd
1788
            True
1789

1790
            >>> s =  ssh(host='example.pwnme')
1791
            >>> homedir = s.pwd()
1792
            >>> _=s.touch('foo')
1793

1794
            >>> _=s.set_working_directory()
1795
            >>> assert s.ls() == b''
1796

1797
            >>> _=s.set_working_directory(homedir)
1798
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1799

1800
            >>> _=s.set_working_directory(symlink=True)
1801
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1802
            >>> assert homedir != s.pwd()
1803

1804
            >>> symlink=os.path.join(homedir,b'*')
1805
            >>> _=s.set_working_directory(symlink=symlink)
1806
            >>> assert b'foo' in s.ls().split(), s.ls().split()
1807
            >>> assert homedir != s.pwd()
1808

1809
            >>> _=s.set_working_directory()
1810
            >>> io = s.system('pwd')
1811
            >>> io.recvallS().strip() == io.cwd
1812
            True
1813
            >>> io.cwd == s.cwd
1814
            True
1815
        """
1816
        status = 0
1✔
1817

1818
        if symlink and not isinstance(symlink, (bytes, str)):
1✔
1819
            symlink = os.path.join(self.pwd(), b'*')
1✔
1820
        if not hasattr(symlink, 'encode') and hasattr(symlink, 'decode'):
1✔
1821
            symlink = symlink.decode('utf-8')
1✔
1822
            
1823
        if isinstance(wd, str):
1✔
1824
            wd = packing._need_bytes(wd, 2, 0x80)
1✔
1825

1826
        if not wd:
1✔
1827
            wd, status = self.run_to_end('x=$(mktemp -d) && cd $x && chmod +x . && echo $PWD', cwd='.')
1✔
1828
            wd = wd.strip()
1✔
1829

1830
            if status:
1!
UNCOV
1831
                self.error("Could not generate a temporary directory (%i)\n%s" % (status, wd))
×
1832

1833
        else:
1834
            cmd = b'ls ' + sh_string(wd)
1✔
1835
            _, status = self.run_to_end(cmd, wd = '.')
1✔
1836

1837
            if status:
1!
UNCOV
1838
                self.error("%r does not appear to exist" % wd)
×
1839

1840
        if not isinstance(wd, str):
1!
1841
            wd = wd.decode('utf-8')
1✔
1842
        self.cwd = wd
1✔
1843

1844
        self.info("Working directory: %r" % self.cwd)
1✔
1845

1846
        if symlink:
1✔
1847
            self.ln('-s', symlink, '.')
1✔
1848

1849
        return wd
1✔
1850

1851
    def write(self, path, data):
1✔
1852
        """Wrapper around upload_data to match :func:`pwnlib.util.misc.write`"""
1853
        data = packing._need_bytes(data)
1✔
1854
        return self.upload_data(data, path)
1✔
1855

1856
    def read(self, path):
1✔
1857
        """Wrapper around download_data to match :func:`pwnlib.util.misc.read`"""
1858
        return self.download_data(path)
1✔
1859

1860
    def _init_remote_platform_info(self):
1✔
1861
        r"""Fills _platform_info, e.g.:
1862

1863
        ::
1864

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

1877
        def preexec():
1✔
UNCOV
1878
            import platform
×
1879
            print('\n'.join(platform.uname()))
×
1880

1881
        with context.quiet:
1✔
1882
            with self.process('true', preexec_fn=preexec) as io:
1✔
1883

1884
                self._platform_info = {
1✔
1885
                    'system': io.recvline().lower().strip().decode(),
1886
                    'node': io.recvline().lower().strip().decode(),
1887
                    'release': io.recvline().lower().strip().decode(),
1888
                    'version': io.recvline().lower().strip().decode(),
1889
                    'machine': io.recvline().lower().strip().decode(),
1890
                    'processor': io.recvline().lower().strip().decode(),
1891
                    'distro': 'Unknown',
1892
                    'distro_ver': ''
1893
                }
1894

1895
            try:
1✔
1896
                if not self.which('lsb_release'):
1!
UNCOV
1897
                    return
×
1898

1899
                with self.process(['lsb_release', '-irs']) as io:
1✔
1900
                    lsb_info = io.recvall().strip().decode()
1✔
1901
                    self._platform_info['distro'], self._platform_info['distro_ver'] = lsb_info.split()
1✔
1902
            except Exception:
1✔
1903
                pass
1✔
1904

1905
    @property
1✔
1906
    def os(self):
1✔
1907
        """:class:`str`: Operating System of the remote machine."""
1908
        try:
1✔
1909
            self._init_remote_platform_info()
1✔
1910
            with context.local(os=self._platform_info['system']):
1✔
1911
                return context.os
1✔
UNCOV
1912
        except Exception:
×
1913
            return "Unknown"
×
1914

1915

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

1926
    @property
1✔
1927
    def bits(self):
1✔
1928
        """:class:`str`: Pointer size of the remote machine."""
UNCOV
1929
        try:
×
1930
            with context.local():
×
1931
                context.clear()
×
1932
                context.arch = self.arch
×
1933
                return context.bits
×
1934
        except Exception:
×
1935
            return context.bits
×
1936

1937
    @property
1✔
1938
    def version(self):
1✔
1939
        """:class:`tuple`: Kernel version of the remote machine."""
1940
        try:
1✔
1941
            self._init_remote_platform_info()
1✔
1942
            vers = self._platform_info['release']
1✔
1943

1944
            # 3.11.0-12-generic
1945
            expr = r'([0-9]+\.?)+'
1✔
1946

1947
            vers = re.search(expr, vers).group()
1✔
1948
            return tuple(map(int, vers.split('.')))
1✔
1949

UNCOV
1950
        except Exception:
×
1951
            return (0,0,0)
×
1952

1953
    @property
1✔
1954
    def distro(self):
1✔
1955
        """:class:`tuple`: Linux distribution name and release."""
1956
        try:
1✔
1957
            self._init_remote_platform_info()
1✔
1958
            return (self._platform_info['distro'], self._platform_info['distro_ver'])
1✔
UNCOV
1959
        except Exception:
×
1960
            return ("Unknown", "Unknown")
×
1961

1962
    @property
1✔
1963
    def aslr(self):
1✔
1964
        """:class:`bool`: Whether ASLR is enabled on the system.
1965

1966
        Example:
1967

1968
            >>> s = ssh("travis", "example.pwnme")
1969
            >>> s.aslr
1970
            True
1971
        """
1972
        if self._aslr is None:
1!
1973
            if self.os != 'linux':
1!
UNCOV
1974
                self.warn_once("Only Linux is supported for ASLR checks.")
×
1975
                self._aslr = False
×
1976

1977
            else:
1978
                with context.quiet:
1✔
1979
                    rvs = self.read('/proc/sys/kernel/randomize_va_space')
1✔
1980

1981
                self._aslr = not rvs.startswith(b'0')
1✔
1982

1983
        return self._aslr
1✔
1984

1985
    @property
1✔
1986
    def aslr_ulimit(self):
1✔
1987
        """:class:`bool`: Whether the entropy of 32-bit processes can be reduced with ulimit."""
1988
        import pwnlib.elf.elf
1✔
1989
        import pwnlib.shellcraft
1✔
1990

1991
        if self._aslr_ulimit is not None:
1!
UNCOV
1992
            return self._aslr_ulimit
×
1993

1994
        # This test must run a 32-bit binary, fix the architecture
1995
        arch = {
1✔
1996
            'amd64': 'i386',
1997
            'aarch64': 'arm'
1998
        }.get(self.arch, self.arch)
1999

2000
        with context.local(arch=arch, bits=32, os=self.os, aslr=True):
1✔
2001
            with context.quiet:
1✔
2002
                try:
1✔
2003
                    sc = pwnlib.shellcraft.cat('/proc/self/maps') \
1✔
2004
                       + pwnlib.shellcraft.exit(0)
2005

2006
                    elf = pwnlib.elf.elf.ELF.from_assembly(sc, shared=True)
1✔
UNCOV
2007
                except Exception:
×
2008
                    self.warn_once("Can't determine ulimit ASLR status")
×
2009
                    self._aslr_ulimit = False
×
2010
                    return self._aslr_ulimit
×
2011

2012
                def preexec():
1✔
UNCOV
2013
                    import resource
×
2014
                    try:
×
2015
                        resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
2016
                    except Exception:
×
2017
                        pass
×
2018

2019
                # Move to a new temporary directory
2020
                cwd = self.cwd
1✔
2021
                tmp = self.set_working_directory()
1✔
2022

2023
                try:
1✔
2024
                    self.upload(elf.path, './aslr-test')
1✔
UNCOV
2025
                except IOError:
×
2026
                    self.warn_once("Couldn't check ASLR ulimit trick")
×
2027
                    self._aslr_ulimit = False
×
2028
                    return False
×
2029

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

2033
                # Move back to the old directory
2034
                self.cwd = cwd
1✔
2035

2036
                # Clean up the files
2037
                self.process(['rm', '-rf', tmp]).wait()
1✔
2038

2039
        # Check for 555555000 (1/3 of the address space for PAE)
2040
        # and for 40000000 (1/3 of the address space with 3BG barrier)
2041
        self._aslr_ulimit = bool(b'55555000' in maps or b'40000000' in maps)
1✔
2042

2043
        return self._aslr_ulimit
1✔
2044

2045
    def _cpuinfo(self):
1✔
2046
        if self._cpuinfo_cache is None:
1✔
2047
            with context.quiet:
1✔
2048
                try:
1✔
2049
                    self._cpuinfo_cache = self.download_data('/proc/cpuinfo', fingerprint=False)
1✔
UNCOV
2050
                except PwnlibException:
×
2051
                    self._cpuinfo_cache = b''
×
2052
        return self._cpuinfo_cache
1✔
2053

2054
    @property
1✔
2055
    def user_shstk(self):
1✔
2056
        """:class:`bool`: Whether userspace shadow stack is supported on the system.
2057

2058
        Example:
2059

2060
            >>> s = ssh("travis", "example.pwnme")
2061
            >>> s.user_shstk # doctest: +SKIP 
2062
            False
2063
        """
2064
        if self._user_shstk is None:
1!
2065
            if self.os != 'linux':
1!
UNCOV
2066
                self.warn_once("Only Linux is supported for userspace shadow stack checks.")
×
2067
                self._user_shstk = False
×
2068

2069
            else:
2070
                cpuinfo = self._cpuinfo()
1✔
2071

2072
                self._user_shstk = b' user_shstk' in cpuinfo
1✔
2073
        return self._user_shstk
1✔
2074

2075
    @property
1✔
2076
    def ibt(self):
1✔
2077
        """:class:`bool`: Whether kernel indirect branch tracking is supported on the system.
2078

2079
        Example:
2080

2081
            >>> s = ssh("travis", "example.pwnme")
2082
            >>> s.ibt
2083
            False
2084
        """
2085
        if self._ibt is None:
1!
2086
            if self.os != 'linux':
1!
UNCOV
2087
                self.warn_once("Only Linux is supported for kernel indirect branch tracking checks.")
×
2088
                self._ibt = False
×
2089

2090
            else:
2091
                cpuinfo = self._cpuinfo()
1✔
2092

2093
                self._ibt = b' ibt ' in cpuinfo or b' ibt\n' in cpuinfo
1✔
2094
        return self._ibt
1✔
2095

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

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

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

2109
        Prints a helpful message about the remote system.
2110

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

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

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

2129
            "ASLR:".ljust(10) + {
2130
                True: green("Enabled"),
2131
                False: red("Disabled")
2132
            }[self.aslr],
2133
            "SHSTK:".ljust(10) + {
2134
                True: green("Enabled"),
2135
                False: red("Disabled")
2136
            }[self.user_shstk],
2137
            "IBT:".ljust(10) + {
2138
                True: green("Enabled"),
2139
                False: red("Disabled")
2140
            }[self.ibt],
2141
        ]
2142

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

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

© 2026 Coveralls, Inc