Skip to content

Commit 2ec3fe6

Browse files
authored
fix(ssh): ssh.which returning None for POSIX paths when running on Windows (#2732)
* fix(ssh): use POSIX separator for path detection in ssh.which On Windows, `os.path.sep` is '\\', which fails to detect POSIX paths. Hardcoding '/' ensures consistent path detection since the `ssh` module targets POSIX-compliant systems. * Update CHANGELOG.md * feat(context): add path_sep and pathlist_sep to context, make the path seperator dependent on the context.os * fix(ssh): declare path separator as private member in ssh * fix(ssh): remote path handling with pathlib * fix(ssh): replace os.path.basename functions with pathlib for path handling * fix(ssh): ensure remote paths are decoded to str
1 parent f4205e5 commit 2ec3fe6

2 files changed

Lines changed: 22 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ The table below shows which release corresponds to each branch, and what date th
136136
- [#2702][2702] ssh: Don't cache username in ssh checksec output
137137
- [#2722][2722] safeeval: allow LIST_APPEND and SET_ADD opcodes (Python 3.14)
138138
- [#2730][2730] Fix atexception handlers in term mode
139+
- [#2732][2732] ssh: fix `ssh.which` returns None for POSIX paths when running on Windows
139140
- [#2733][2733] loongarch64: fix incorrect mov assembly template
140141
- [#2746][2746] elf: point people at libc_start_main_return when they look up __libc_start_main_ret
141142

@@ -197,6 +198,7 @@ The table below shows which release corresponds to each branch, and what date th
197198
[2722]: https://github.com/Gallopsled/pwntools/pull/2722
198199
[2725]: https://github.com/Gallopsled/pwntools/pull/2725
199200
[2730]: https://github.com/Gallopsled/pwntools/pull/2730
201+
[2732]: https://github.com/Gallopsled/pwntools/pull/2732
200202
[2733]: https://github.com/Gallopsled/pwntools/pull/2733
201203
[2739]: https://github.com/Gallopsled/pwntools/pull/2739
202204
[2740]: https://github.com/Gallopsled/pwntools/pull/2740

pwnlib/tubes/ssh.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
import pathlib
34
import re
45
import shutil
56
import string
@@ -592,6 +593,8 @@ class ssh(Timeout, Logger):
592593
pid = None
593594

594595
_cwd = '.'
596+
_sep = '/'
597+
_pathlib = pathlib.PurePosixPath
595598
_tried_sftp = False
596599

597600
def __init__(self, user=None, host=None, port=22, password=None, key=None,
@@ -696,6 +699,10 @@ def __init__(self, user=None, host=None, port=22, password=None, key=None,
696699

697700
misc.mkdir_p(self._cachedir)
698701

702+
if context.os == 'windows':
703+
self._sep = '\\'
704+
self._pathlib = pathlib.PureWindowsPath
705+
699706
import paramiko
700707

701708
# Make a basic attempt to parse the ssh_config file
@@ -1122,14 +1129,15 @@ def which(self, program):
11221129
system which adds the current working directory to the end of ``$PATH``.
11231130
"""
11241131
# If name is a path, do not attempt to resolve it.
1125-
if os.path.sep in program:
1132+
if self._sep in program:
11261133
return program
11271134

11281135
program = packing._encode(program)
1136+
pathsep = self._sep.encode()
11291137

11301138
result = self.system(b'export PATH=$PATH:$PWD; command -v ' + program).recvall().strip()
11311139

1132-
if (b'/' + program) not in result:
1140+
if (pathsep + program) not in result:
11331141
return None
11341142

11351143
return packing._decode(result)
@@ -1471,8 +1479,7 @@ def _download_to_cache(self, remote, p, fingerprint=True):
14711479

14721480
fingerprint = fingerprint and self._get_fingerprint(remote) or None
14731481
if fingerprint is None:
1474-
local = os.path.normpath(remote)
1475-
local = os.path.basename(local)
1482+
local = self._pathlib(remote).name
14761483
local += time.strftime('-%Y-%m-%d-%H%M%S')
14771484
local = os.path.join(self._cachedir, local)
14781485

@@ -1541,7 +1548,8 @@ def download_file(self, remote, local = None):
15411548

15421549

15431550
if not local:
1544-
local = os.path.basename(os.path.normpath(remote))
1551+
remote_str = remote.decode() if not hasattr(remote, 'encode') else remote
1552+
local = self._pathlib(remote_str).name
15451553

15461554
with self.progress('Downloading %r to %r' % (remote, local)) as p:
15471555
local_tmp = self._download_to_cache(remote, p)
@@ -1617,9 +1625,12 @@ def upload_data(self, data, remote):
16171625
Hello, world
16181626
"""
16191627
data = packing._need_bytes(data)
1628+
if not hasattr(remote, 'encode'):
1629+
remote = remote.decode('utf-8')
16201630
# If a relative path was provided, prepend the cwd
1621-
if os.path.normpath(remote) == os.path.basename(remote):
1622-
remote = os.path.join(self.cwd, remote)
1631+
remote_path = self._pathlib(remote)
1632+
if str(remote_path) == remote_path.name:
1633+
remote = str(self._pathlib(self.cwd) / remote)
16231634

16241635
if self.sftp:
16251636
flo = BytesIO(data)
@@ -1650,9 +1661,7 @@ def upload_file(self, filename, remote = None):
16501661

16511662

16521663
if remote is None:
1653-
remote = os.path.normpath(filename)
1654-
remote = os.path.basename(remote)
1655-
remote = os.path.join(self.cwd, remote)
1664+
remote = str(self._pathlib(self.cwd) / self._pathlib(filename).name)
16561665

16571666
with open(filename, 'rb') as fd:
16581667
data = fd.read()
@@ -1909,7 +1918,7 @@ def set_working_directory(self, wd = None, symlink = False):
19091918
status = 0
19101919

19111920
if symlink and not isinstance(symlink, (bytes, str)):
1912-
symlink = os.path.join(self.pwd(), b'*')
1921+
symlink = str(self._pathlib(self.pwd().decode()) / '*')
19131922
if not hasattr(symlink, 'encode') and hasattr(symlink, 'decode'):
19141923
symlink = symlink.decode('utf-8')
19151924

0 commit comments

Comments
 (0)