sshcatch 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sshcatch-0.2.2 → sshcatch-0.3.0}/CHANGELOG.md +29 -0
- {sshcatch-0.2.2 → sshcatch-0.3.0}/PKG-INFO +22 -18
- {sshcatch-0.2.2 → sshcatch-0.3.0}/README.md +20 -16
- {sshcatch-0.2.2 → sshcatch-0.3.0}/pyproject.toml +5 -1
- {sshcatch-0.2.2 → sshcatch-0.3.0}/sshcatch.py +151 -29
- {sshcatch-0.2.2 → sshcatch-0.3.0}/.gitignore +0 -0
- {sshcatch-0.2.2 → sshcatch-0.3.0}/LICENSE +0 -0
|
@@ -4,6 +4,35 @@ All notable changes to **sshcatch** are documented here.
|
|
|
4
4
|
This project follows [Keep a Changelog](https://keepachangelog.com/) and
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-08-02
|
|
8
|
+
|
|
9
|
+
Pose as another SSH server.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`--mimic debian` / `--mimic dropbear`** poses as a real OpenSSH 8.4p1 (Debian 11) or
|
|
14
|
+
Dropbear 2024.86 server: version banner, KEXINIT/HASSH, offered host keys and auth
|
|
15
|
+
methods match a capture of the real server. After authentication `sshcatch` differs from a
|
|
16
|
+
real server anyway, so `--mimic` only works pre-auth. Every modification to asyncssh,
|
|
17
|
+
the reasoning and the captures are in
|
|
18
|
+
[`mimic-refs/mimic-notes.md`](https://github.com/LorenzMap/sshcatch/blob/main/mimic-refs/mimic-notes.md).
|
|
19
|
+
- `select_host_keys()` offers only the host-key types the mimicked server has, in its order.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **asyncssh is pinned to 2.24.0 exactly** - `--mimic` patches library internals, so a
|
|
24
|
+
different version could break the disguise silently. If a patched internal is gone,
|
|
25
|
+
sshcatch refuses to start.
|
|
26
|
+
- The sdist ships only what is needed to install the package (no `mimic-refs/`).
|
|
27
|
+
- The startup summary shows the active mimic preset.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- **`--version-banner` presets** (`ubuntu`, `debian`, `dropbear`, `windows`, `macos`)
|
|
32
|
+
are dropped and the flag now takes a literal string only, `--mimic` sets a fitting
|
|
33
|
+
banner itself.
|
|
34
|
+
|
|
35
|
+
|
|
7
36
|
## [0.2.2] - 2026-07-31
|
|
8
37
|
|
|
9
38
|
Multiple host keys now used as default.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sshcatch
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Quick-deploy SSH server for tunneling and simple SCP transfers - never opens a shell.
|
|
5
5
|
Project-URL: Homepage, https://github.com/LorenzMap/sshcatch
|
|
6
6
|
Project-URL: Repository, https://github.com/LorenzMap/sshcatch
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Security
|
|
22
22
|
Classifier: Topic :: System :: Networking
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: asyncssh
|
|
24
|
+
Requires-Dist: asyncssh==2.24.0
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
27
|
# sshcatch
|
|
@@ -50,7 +50,7 @@ to test.
|
|
|
50
50
|
- configuring it through `sshd_configs` is a pain
|
|
51
51
|
- multiple use-cases require different configs (tunnel direction? sftp direction? different ports?)
|
|
52
52
|
- logins are controlled by the OS so a user must be created (and secured)
|
|
53
|
-
-
|
|
53
|
+
- ForceCommands need to be set up to restrict the shell
|
|
54
54
|
|
|
55
55
|
- My solution: `sshcatch`
|
|
56
56
|
- Simply configure through clear flags and arguments on the commandline
|
|
@@ -124,9 +124,12 @@ Renames, deletes and directory removal are denied.
|
|
|
124
124
|
|
|
125
125
|
## Word of Warning
|
|
126
126
|
|
|
127
|
-
Only using `--version-banner` obviously isn't enough deception
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
Only using `--version-banner` obviously isn't enough deception against a sufficiently
|
|
128
|
+
sophisticated observer because some of the data transferred in cleartext on the wire
|
|
129
|
+
during connection establishment is a clear tell. Use `--mimic` if that's something you
|
|
130
|
+
want to try and dodge in an engagement. Check out
|
|
131
|
+
[`mimic-refs/mimic-notes.md`](https://github.com/LorenzMap/sshcatch/blob/main/mimic-refs/mimic-notes.md)
|
|
132
|
+
for details about `--mimic`.
|
|
130
133
|
|
|
131
134
|
Also: `sshcatch` is **NOT** designed to be a **honeypot**. Advanced deception, long-term logging
|
|
132
135
|
and everything else a real honeypot needs are deliberately out of scope. There are other
|
|
@@ -165,7 +168,7 @@ and saving them into a file:
|
|
|
165
168
|
|
|
166
169
|
```
|
|
167
170
|
# Server
|
|
168
|
-
sshcatch -1 -u arthur:42 --version-banner
|
|
171
|
+
sshcatch -1 -u arthur:42 --version-banner 'heart_of_gold' \
|
|
169
172
|
--pre-auth-banner "What is the answer to life the universe and everything" \
|
|
170
173
|
--post-auth-banner "flag{So_Long_and_Thanks_for_All_the_Fish}" \
|
|
171
174
|
-o sshcatch.log -t
|
|
@@ -173,11 +176,11 @@ sshcatch -1 -u arthur:42 --version-banner debian \
|
|
|
173
176
|
|
|
174
177
|
|
|
175
178
|
My favorite one: Reverse tunnel and SCP uploads for the keys in
|
|
176
|
-
`./
|
|
179
|
+
`./authorized_keys` on port 2222:
|
|
177
180
|
|
|
178
181
|
```
|
|
179
182
|
# Server
|
|
180
|
-
sshcatch --reverse --authorized-keys ./
|
|
183
|
+
sshcatch --reverse --authorized-keys ./authorized_keys --scp-upload -p 2222
|
|
181
184
|
|
|
182
185
|
# Client
|
|
183
186
|
ssh -NR 9000:localhost:22 user@host -p 2222 # reverse tunnel
|
|
@@ -191,15 +194,15 @@ scp -P 2222 loot.tar user@host:. # upload
|
|
|
191
194
|
The full reference below is `sshcatch --help`:
|
|
192
195
|
|
|
193
196
|
```
|
|
194
|
-
usage: sshcatch [-h] [--help] [-p PORT] [-b BIND] [-1] [--
|
|
195
|
-
[--version] [-u USER:PASS] [--open-auth]
|
|
197
|
+
usage: sshcatch [-h] [--help] [-p PORT] [-b BIND] [-1] [--mimic PRESET]
|
|
198
|
+
[--host-key FILE] [--version] [-u USER:PASS] [--open-auth]
|
|
196
199
|
[--authorized-keys FILE] [--forward] [--reverse]
|
|
197
200
|
[--scp-upload] [--scp-download] [--scp-dir DIR]
|
|
198
201
|
[--version-banner STRING] [--pre-auth-banner STRING]
|
|
199
202
|
[--post-auth-banner STRING] [-q | -v] [-o FILE] [-t] [--plain]
|
|
200
203
|
|
|
201
204
|
sshcatch - a quick-deploy SSH server for tunneling (local/remote/dynamic)
|
|
202
|
-
and simple SCP transfers (NEVER opens a shell!)
|
|
205
|
+
and simple SCP/SFTP transfers (NEVER opens a shell!)
|
|
203
206
|
By default all features are disabled. Use flags to enable features.
|
|
204
207
|
|
|
205
208
|
options:
|
|
@@ -209,6 +212,11 @@ options:
|
|
|
209
212
|
-b BIND, --bind BIND bind address (default: all IPv4/v6 interfaces)
|
|
210
213
|
-1, --single close the listener after first successful auth (and
|
|
211
214
|
exit when that connection ends)
|
|
215
|
+
--mimic PRESET pose as another SSH server - presets (case-
|
|
216
|
+
insensitive): debian, dropbear, none - match the
|
|
217
|
+
preset's pre-auth (banner, KEXINIT, server-sig-algs,
|
|
218
|
+
...) exactly - banner can be overridden by --version-
|
|
219
|
+
banner - check Github repository for details
|
|
212
220
|
--host-key FILE server host key file, may hold several keys - auto-
|
|
213
221
|
generated if missing - uses ./sshcatch_host_key by
|
|
214
222
|
default
|
|
@@ -237,10 +245,7 @@ SCP / SFTP file transfer:
|
|
|
237
245
|
|
|
238
246
|
banners:
|
|
239
247
|
--version-banner STRING
|
|
240
|
-
|
|
241
|
-
glance deception, it can still be identified as
|
|
242
|
-
asyncssh - presets (case-insensitive): ubuntu, debian,
|
|
243
|
-
dropbear, windows, macos
|
|
248
|
+
manually set 'SSH-2.0-STRING' version banner
|
|
244
249
|
--pre-auth-banner STRING
|
|
245
250
|
banner shown to every client before login
|
|
246
251
|
--post-auth-banner STRING
|
|
@@ -262,8 +267,7 @@ examples: (also check README on Github)
|
|
|
262
267
|
sshcatch --open-auth --forward Allow ANYONE! to tunnel through this SSH server
|
|
263
268
|
# My favorite one
|
|
264
269
|
# Allows reverse tunnels and uploads via SCP for the keys in ./authorized_keys
|
|
265
|
-
|
|
266
|
-
sshcatch --reverse --authorized-keys ./authorized-keys --scp-upload --version-banner ubuntu -p 2222
|
|
270
|
+
sshcatch --reverse --authorized-keys ./authorized_keys --scp-upload -p 2222
|
|
267
271
|
```
|
|
268
272
|
|
|
269
273
|
## License
|
|
@@ -24,7 +24,7 @@ to test.
|
|
|
24
24
|
- configuring it through `sshd_configs` is a pain
|
|
25
25
|
- multiple use-cases require different configs (tunnel direction? sftp direction? different ports?)
|
|
26
26
|
- logins are controlled by the OS so a user must be created (and secured)
|
|
27
|
-
-
|
|
27
|
+
- ForceCommands need to be set up to restrict the shell
|
|
28
28
|
|
|
29
29
|
- My solution: `sshcatch`
|
|
30
30
|
- Simply configure through clear flags and arguments on the commandline
|
|
@@ -98,9 +98,12 @@ Renames, deletes and directory removal are denied.
|
|
|
98
98
|
|
|
99
99
|
## Word of Warning
|
|
100
100
|
|
|
101
|
-
Only using `--version-banner` obviously isn't enough deception
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
Only using `--version-banner` obviously isn't enough deception against a sufficiently
|
|
102
|
+
sophisticated observer because some of the data transferred in cleartext on the wire
|
|
103
|
+
during connection establishment is a clear tell. Use `--mimic` if that's something you
|
|
104
|
+
want to try and dodge in an engagement. Check out
|
|
105
|
+
[`mimic-refs/mimic-notes.md`](https://github.com/LorenzMap/sshcatch/blob/main/mimic-refs/mimic-notes.md)
|
|
106
|
+
for details about `--mimic`.
|
|
104
107
|
|
|
105
108
|
Also: `sshcatch` is **NOT** designed to be a **honeypot**. Advanced deception, long-term logging
|
|
106
109
|
and everything else a real honeypot needs are deliberately out of scope. There are other
|
|
@@ -139,7 +142,7 @@ and saving them into a file:
|
|
|
139
142
|
|
|
140
143
|
```
|
|
141
144
|
# Server
|
|
142
|
-
sshcatch -1 -u arthur:42 --version-banner
|
|
145
|
+
sshcatch -1 -u arthur:42 --version-banner 'heart_of_gold' \
|
|
143
146
|
--pre-auth-banner "What is the answer to life the universe and everything" \
|
|
144
147
|
--post-auth-banner "flag{So_Long_and_Thanks_for_All_the_Fish}" \
|
|
145
148
|
-o sshcatch.log -t
|
|
@@ -147,11 +150,11 @@ sshcatch -1 -u arthur:42 --version-banner debian \
|
|
|
147
150
|
|
|
148
151
|
|
|
149
152
|
My favorite one: Reverse tunnel and SCP uploads for the keys in
|
|
150
|
-
`./
|
|
153
|
+
`./authorized_keys` on port 2222:
|
|
151
154
|
|
|
152
155
|
```
|
|
153
156
|
# Server
|
|
154
|
-
sshcatch --reverse --authorized-keys ./
|
|
157
|
+
sshcatch --reverse --authorized-keys ./authorized_keys --scp-upload -p 2222
|
|
155
158
|
|
|
156
159
|
# Client
|
|
157
160
|
ssh -NR 9000:localhost:22 user@host -p 2222 # reverse tunnel
|
|
@@ -165,15 +168,15 @@ scp -P 2222 loot.tar user@host:. # upload
|
|
|
165
168
|
The full reference below is `sshcatch --help`:
|
|
166
169
|
|
|
167
170
|
```
|
|
168
|
-
usage: sshcatch [-h] [--help] [-p PORT] [-b BIND] [-1] [--
|
|
169
|
-
[--version] [-u USER:PASS] [--open-auth]
|
|
171
|
+
usage: sshcatch [-h] [--help] [-p PORT] [-b BIND] [-1] [--mimic PRESET]
|
|
172
|
+
[--host-key FILE] [--version] [-u USER:PASS] [--open-auth]
|
|
170
173
|
[--authorized-keys FILE] [--forward] [--reverse]
|
|
171
174
|
[--scp-upload] [--scp-download] [--scp-dir DIR]
|
|
172
175
|
[--version-banner STRING] [--pre-auth-banner STRING]
|
|
173
176
|
[--post-auth-banner STRING] [-q | -v] [-o FILE] [-t] [--plain]
|
|
174
177
|
|
|
175
178
|
sshcatch - a quick-deploy SSH server for tunneling (local/remote/dynamic)
|
|
176
|
-
and simple SCP transfers (NEVER opens a shell!)
|
|
179
|
+
and simple SCP/SFTP transfers (NEVER opens a shell!)
|
|
177
180
|
By default all features are disabled. Use flags to enable features.
|
|
178
181
|
|
|
179
182
|
options:
|
|
@@ -183,6 +186,11 @@ options:
|
|
|
183
186
|
-b BIND, --bind BIND bind address (default: all IPv4/v6 interfaces)
|
|
184
187
|
-1, --single close the listener after first successful auth (and
|
|
185
188
|
exit when that connection ends)
|
|
189
|
+
--mimic PRESET pose as another SSH server - presets (case-
|
|
190
|
+
insensitive): debian, dropbear, none - match the
|
|
191
|
+
preset's pre-auth (banner, KEXINIT, server-sig-algs,
|
|
192
|
+
...) exactly - banner can be overridden by --version-
|
|
193
|
+
banner - check Github repository for details
|
|
186
194
|
--host-key FILE server host key file, may hold several keys - auto-
|
|
187
195
|
generated if missing - uses ./sshcatch_host_key by
|
|
188
196
|
default
|
|
@@ -211,10 +219,7 @@ SCP / SFTP file transfer:
|
|
|
211
219
|
|
|
212
220
|
banners:
|
|
213
221
|
--version-banner STRING
|
|
214
|
-
|
|
215
|
-
glance deception, it can still be identified as
|
|
216
|
-
asyncssh - presets (case-insensitive): ubuntu, debian,
|
|
217
|
-
dropbear, windows, macos
|
|
222
|
+
manually set 'SSH-2.0-STRING' version banner
|
|
218
223
|
--pre-auth-banner STRING
|
|
219
224
|
banner shown to every client before login
|
|
220
225
|
--post-auth-banner STRING
|
|
@@ -236,8 +241,7 @@ examples: (also check README on Github)
|
|
|
236
241
|
sshcatch --open-auth --forward Allow ANYONE! to tunnel through this SSH server
|
|
237
242
|
# My favorite one
|
|
238
243
|
# Allows reverse tunnels and uploads via SCP for the keys in ./authorized_keys
|
|
239
|
-
|
|
240
|
-
sshcatch --reverse --authorized-keys ./authorized-keys --scp-upload --version-banner ubuntu -p 2222
|
|
244
|
+
sshcatch --reverse --authorized-keys ./authorized_keys --scp-upload -p 2222
|
|
241
245
|
```
|
|
242
246
|
|
|
243
247
|
## License
|
|
@@ -25,7 +25,8 @@ classifiers = [
|
|
|
25
25
|
"Topic :: Security",
|
|
26
26
|
"Topic :: System :: Networking",
|
|
27
27
|
]
|
|
28
|
-
|
|
28
|
+
# Pinned exactly due to mimic monkey-patches on asyncssh internals (see mimic-refs/mimic-notes.md)
|
|
29
|
+
dependencies = ["asyncssh==2.24.0"]
|
|
29
30
|
|
|
30
31
|
[project.urls]
|
|
31
32
|
Homepage = "https://github.com/LorenzMap/sshcatch"
|
|
@@ -40,3 +41,6 @@ path = "sshcatch.py"
|
|
|
40
41
|
|
|
41
42
|
[tool.hatch.build.targets.wheel]
|
|
42
43
|
only-include = ["sshcatch.py"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
only-include = ["sshcatch.py", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
sshcatch - a quick-deploy SSH server for tunneling (local/remote/dynamic)
|
|
4
|
-
and simple SCP transfers (NEVER opens a shell!).
|
|
4
|
+
and simple SCP/SFTP transfers (NEVER opens a shell!).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import asyncio
|
|
9
|
+
import functools
|
|
9
10
|
import logging
|
|
10
11
|
import os
|
|
11
12
|
import posixpath
|
|
@@ -17,7 +18,7 @@ from itertools import count
|
|
|
17
18
|
|
|
18
19
|
import asyncssh
|
|
19
20
|
|
|
20
|
-
__version__ = "0.
|
|
21
|
+
__version__ = "0.3.0"
|
|
21
22
|
|
|
22
23
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
23
24
|
|
|
@@ -252,8 +253,7 @@ class SFTPCatchServer(asyncssh.SFTPServer):
|
|
|
252
253
|
self._ensure_parent(new)
|
|
253
254
|
target = old.decode(errors="replace")
|
|
254
255
|
Path(os.fsdecode(self.map_path(new))).write_text(f"symlink -> {target}\n")
|
|
255
|
-
self._log_scp(f"SYMLINK {self._local_path(new)} -> {target} (placeholder)",
|
|
256
|
-
logging.WARNING)
|
|
256
|
+
self._log_scp(f"SYMLINK {self._local_path(new)} -> {target} (placeholder)", logging.WARNING)
|
|
257
257
|
|
|
258
258
|
def setstat(self, path, attrs):
|
|
259
259
|
# Allow (upload only): perms/timestamps on uploaded files
|
|
@@ -470,6 +470,11 @@ def make_server_factory(args, single_future=None):
|
|
|
470
470
|
# always accept passwords so we can log them
|
|
471
471
|
return True
|
|
472
472
|
|
|
473
|
+
def kbdint_auth_supported(self):
|
|
474
|
+
# force publickey,password auth for mimic (drop keyboard-interactive)
|
|
475
|
+
if not args.mimic == "none": return False
|
|
476
|
+
return super().kbdint_auth_supported()
|
|
477
|
+
|
|
473
478
|
def validate_password(self, username, password):
|
|
474
479
|
accepted = accept_password(username, password)
|
|
475
480
|
if accepted:
|
|
@@ -545,9 +550,8 @@ def make_server_factory(args, single_future=None):
|
|
|
545
550
|
|
|
546
551
|
# ── Server start ──────────────────────────────────────────────────────
|
|
547
552
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
553
|
+
GENERATE_KEYS = ("ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256")
|
|
554
|
+
GENERATE_OPTS = {"ssh-rsa": {"key_size": 3072}}
|
|
551
555
|
|
|
552
556
|
async def start_server(args):
|
|
553
557
|
# Handle Host keys
|
|
@@ -563,13 +567,15 @@ async def start_server(args):
|
|
|
563
567
|
if not host_keys: raise ValueError(f"No usable host key in {key_path}")
|
|
564
568
|
log_info(f"Read host key: {key_path} ({len(host_keys)} key{'s'*(len(host_keys)!=1)})")
|
|
565
569
|
else:
|
|
566
|
-
host_keys = [asyncssh.generate_private_key(a, **
|
|
567
|
-
for a in HOST_KEY_ALGS]
|
|
570
|
+
host_keys = [asyncssh.generate_private_key(a, **GENERATE_OPTS.get(a, {})) for a in GENERATE_KEYS]
|
|
568
571
|
key_path.write_bytes(b"".join(k.export_private_key() for k in host_keys))
|
|
569
572
|
log_info(f"Generated host keys file: {key_path}")
|
|
570
573
|
if os.name == "posix": key_path.chmod(0o600)
|
|
571
574
|
else: log_info("Please make sure the permissions on the host key are securely set!", level=logging.WARNING)
|
|
572
|
-
|
|
575
|
+
|
|
576
|
+
# Choose which keys to expose and what host-key algorithms to advertise
|
|
577
|
+
exposed_keys, host_key_algs = select_host_keys(host_keys, args.mimic)
|
|
578
|
+
fingerprints = [(k.get_algorithm(), k.get_fingerprint()) for k in exposed_keys]
|
|
573
579
|
|
|
574
580
|
# for single-connection mode - resolving releases the bind on the listen port
|
|
575
581
|
single_future = asyncio.get_running_loop().create_future() if args.single else None
|
|
@@ -578,12 +584,15 @@ async def start_server(args):
|
|
|
578
584
|
server_factory, n_users, n_keys = make_server_factory(args, single_future)
|
|
579
585
|
opts = {
|
|
580
586
|
"server_factory": server_factory,
|
|
581
|
-
"server_host_keys":
|
|
587
|
+
"server_host_keys": exposed_keys,
|
|
582
588
|
# SFTPv3 only so all transfers use open() and not open56()
|
|
583
589
|
"sftp_version": 3,
|
|
584
590
|
}
|
|
585
|
-
if args.version_banner:
|
|
591
|
+
if args.version_banner:
|
|
586
592
|
opts["server_version"] = args.version_banner
|
|
593
|
+
if args.mimic != "none":
|
|
594
|
+
opts.update(MIMIC_PRESETS[args.mimic][1])
|
|
595
|
+
apply_mimic_patches(args.mimic, host_key_algs)
|
|
587
596
|
|
|
588
597
|
has_scp = args.scp_upload or args.scp_download
|
|
589
598
|
if has_scp:
|
|
@@ -643,10 +652,12 @@ async def start_server(args):
|
|
|
643
652
|
if args.single: summary.append("Mode .......... single-connection")
|
|
644
653
|
if has_scp: summary.append(f"SCP dir ....... {args.scp_dir.resolve()}")
|
|
645
654
|
summary.append(f"Version ....... SSH-2.0-{options.version.decode()}")
|
|
655
|
+
summary.append(f"Mimic ......... {args.mimic}")
|
|
646
656
|
if args.pre_auth_banner: summary.append(f"Pre-auth ...... {banner_preview(args.pre_auth_banner)}")
|
|
647
657
|
if args.post_auth_banner: summary.append(f"Post-auth ..... {banner_preview(args.post_auth_banner)}")
|
|
648
658
|
summary.append(f"Key file ...... {key_path}")
|
|
649
659
|
for algo, fp in fingerprints: summary.append(f"Host key ...... {fp} ({algo})")
|
|
660
|
+
summary.append(f"\n")
|
|
650
661
|
log_info("sshcatch\n" + "\n".join(f" {line}" for line in summary), level=logging.WARNING)
|
|
651
662
|
|
|
652
663
|
acceptor = await asyncssh.listen(host=args.bind, port=args.port, options=options)
|
|
@@ -665,18 +676,124 @@ async def start_server(args):
|
|
|
665
676
|
|
|
666
677
|
# ── Main ──────────────────────────────────────────────────────────────
|
|
667
678
|
|
|
668
|
-
#
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
"
|
|
679
|
+
# ── Mimic presets ─────────────────────────────────────────────────────
|
|
680
|
+
# - algorithm lists below shape the cleartext KEXINIT
|
|
681
|
+
# - apply_mimic_patches() handles the monkey-patches of asyncssh
|
|
682
|
+
# - for full reasoning see 'mimic-refs/mimic-notes.md'
|
|
683
|
+
|
|
684
|
+
DEBIAN_ALGS = {
|
|
685
|
+
"kex_algs": ["curve25519-sha256", "curve25519-sha256@libssh.org",
|
|
686
|
+
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
|
|
687
|
+
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group16-sha512",
|
|
688
|
+
"diffie-hellman-group18-sha512", "diffie-hellman-group14-sha256"],
|
|
689
|
+
"encryption_algs": ["chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr",
|
|
690
|
+
"aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"],
|
|
691
|
+
"mac_algs": ["umac-64-etm@openssh.com", "umac-128-etm@openssh.com",
|
|
692
|
+
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com",
|
|
693
|
+
"hmac-sha1-etm@openssh.com", "umac-64@openssh.com", "umac-128@openssh.com",
|
|
694
|
+
"hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
|
|
695
|
+
"compression_algs": ["none", "zlib@openssh.com"],
|
|
696
|
+
"signature_algs": ["ssh-ed25519", "sk-ssh-ed25519@openssh.com", "ssh-rsa",
|
|
697
|
+
"rsa-sha2-256", "rsa-sha2-512", "ssh-dss",
|
|
698
|
+
"ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521",
|
|
699
|
+
"sk-ecdsa-sha2-nistp256@openssh.com",
|
|
700
|
+
"webauthn-sk-ecdsa-sha2-nistp256@openssh.com"],
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
DROPBEAR_ALGS = {
|
|
704
|
+
"kex_algs": ["curve25519-sha256", "curve25519-sha256@libssh.org",
|
|
705
|
+
"ecdh-sha2-nistp521", "ecdh-sha2-nistp384", "ecdh-sha2-nistp256",
|
|
706
|
+
"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"],
|
|
707
|
+
"encryption_algs": ["chacha20-poly1305@openssh.com", "aes128-ctr", "aes256-ctr"],
|
|
708
|
+
"mac_algs": ["hmac-sha1", "hmac-sha2-256"],
|
|
709
|
+
"compression_algs": ["zlib@openssh.com", "none"],
|
|
710
|
+
"signature_algs": ["ssh-ed25519", "sk-ssh-ed25519@openssh.com",
|
|
711
|
+
"ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521",
|
|
712
|
+
"sk-ecdsa-sha2-nistp256@openssh.com", "rsa-sha2-256", "ssh-rsa"],
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
MIMIC_PRESETS = {
|
|
716
|
+
"debian": ("OpenSSH_8.4p1 Debian-5+deb11u7", DEBIAN_ALGS), # Debian 11 (bullseye)
|
|
717
|
+
"dropbear": ("dropbear_2024.86", DROPBEAR_ALGS), # Dropbear 2024.86
|
|
718
|
+
"none": None # asyncssh defaults
|
|
675
719
|
}
|
|
676
720
|
|
|
721
|
+
ADVERTISED_HOSTKEY_ALGS = {
|
|
722
|
+
"ssh-ed25519": [b"ssh-ed25519"],
|
|
723
|
+
"ecdsa-sha2-nistp256": [b"ecdsa-sha2-nistp256"],
|
|
724
|
+
"ssh-rsa": [b"rsa-sha2-512", b"rsa-sha2-256", b"ssh-rsa"],
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
MIMIC_HOSTKEYS = {
|
|
728
|
+
"debian": ["ssh-rsa", "ecdsa-sha2-nistp256", "ssh-ed25519"], # debian: rsa+ecdsa+ed25519
|
|
729
|
+
"dropbear": ["ssh-ed25519"], # dropbear: ed25519 only
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def select_host_keys(host_keys, mimic):
|
|
734
|
+
# Select fitting host keys for the mimicked server and order them in the default way
|
|
735
|
+
keys_by_algo = {k.get_algorithm(): k for k in host_keys}
|
|
736
|
+
wanted = MIMIC_HOSTKEYS.get(mimic, [])
|
|
737
|
+
missing = [a for a in wanted if a not in keys_by_algo]
|
|
738
|
+
if missing:
|
|
739
|
+
log_info(f"Host-keys missing: {missing} - a real {mimic} server provides "
|
|
740
|
+
f"those (delete the host-key file to auto-generate all types)",
|
|
741
|
+
level=logging.WARNING)
|
|
742
|
+
order = [a for a in wanted if a in keys_by_algo]
|
|
743
|
+
# return (keys_to_expose, advertised_algs)
|
|
744
|
+
if not order: return host_keys, None
|
|
745
|
+
return ([keys_by_algo[a] for a in order],
|
|
746
|
+
[alg for a in order for alg in ADVERTISED_HOSTKEY_ALGS[a]])
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def apply_mimic_patches(mimic, host_key_algs=None):
|
|
750
|
+
# Monkey-patch asyncssh internals so the cleartext/pre-auth transport matches the mimicked server
|
|
751
|
+
from asyncssh.connection import SSHConnection, SSHServerConnection
|
|
752
|
+
from asyncssh.constants import MSG_IGNORE
|
|
753
|
+
|
|
754
|
+
def wrap(cls, name, fn):
|
|
755
|
+
orig = getattr(cls, name, None)
|
|
756
|
+
if orig is None:
|
|
757
|
+
raise RuntimeError(
|
|
758
|
+
f"Cannot apply --mimic {mimic}: asyncssh internal '{name}' changed. "
|
|
759
|
+
f"Refusing to run a fingerprintable disguise (use '--mimic none' to bypass)")
|
|
760
|
+
setattr(cls, name, functools.partialmethod(fn, orig))
|
|
761
|
+
|
|
762
|
+
def kex(self, orig):
|
|
763
|
+
# 1. Modify KEXINIT to match mimicked server
|
|
764
|
+
algs = [a for a in orig(self) if a != b"ext-info-s"]
|
|
765
|
+
if self.is_server() and mimic == "dropbear":
|
|
766
|
+
algs = [b"kexguess2@matt.ucc.asn.au"] + algs
|
|
767
|
+
return algs
|
|
768
|
+
wrap(SSHConnection, "_get_extra_kex_algs", kex)
|
|
769
|
+
|
|
770
|
+
def ext_info(self, orig):
|
|
771
|
+
# 2. Strip asyncssh 'global-requests-ok' from EXT_INFO
|
|
772
|
+
self._extensions_to_send.pop(b"global-requests-ok", None)
|
|
773
|
+
return orig(self)
|
|
774
|
+
wrap(SSHConnection, "_send_ext_info", ext_info)
|
|
775
|
+
|
|
776
|
+
def send_packet(self, orig, pkttype, *a, **kw):
|
|
777
|
+
# 3. Suppress asyncssh's SSH_MSG_IGNORE traffic-analysis chaff (added with
|
|
778
|
+
# OpenSSH 9.5). Safe to drop all because asyncssh only sends MSG_IGNORE
|
|
779
|
+
# as this chaff using self.send_packet() before each real packet
|
|
780
|
+
if pkttype != MSG_IGNORE:
|
|
781
|
+
return orig(self, pkttype, *a, **kw)
|
|
782
|
+
wrap(SSHConnection, "send_packet", send_packet)
|
|
783
|
+
|
|
784
|
+
if host_key_algs is not None:
|
|
785
|
+
# 4. Pin the advertised server host-key algorithms
|
|
786
|
+
algs = list(host_key_algs)
|
|
787
|
+
def init(self, orig, *a, **kw):
|
|
788
|
+
orig(self, *a, **kw)
|
|
789
|
+
self._server_host_key_algs = [x for x in algs if x in self._server_host_keys]
|
|
790
|
+
wrap(SSHServerConnection, "__init__", init)
|
|
791
|
+
|
|
792
|
+
# ── Startup and arguments ─────────────────────────────────────────────
|
|
793
|
+
|
|
677
794
|
_description="""\
|
|
678
795
|
sshcatch - a quick-deploy SSH server for tunneling (local/remote/dynamic)
|
|
679
|
-
and simple SCP transfers (NEVER opens a shell!)
|
|
796
|
+
and simple SCP/SFTP transfers (NEVER opens a shell!)
|
|
680
797
|
By default all features are disabled. Use flags to enable features.
|
|
681
798
|
"""
|
|
682
799
|
|
|
@@ -691,8 +808,7 @@ examples: (also check README on Github)
|
|
|
691
808
|
%(prog)s --open-auth --forward Allow ANYONE! to tunnel through this SSH server
|
|
692
809
|
# My favorite one
|
|
693
810
|
# Allows reverse tunnels and uploads via SCP for the keys in ./authorized_keys
|
|
694
|
-
|
|
695
|
-
%(prog)s --reverse --authorized-keys ./authorized-keys --scp-upload --version-banner ubuntu -p 2222
|
|
811
|
+
%(prog)s --reverse --authorized-keys ./authorized_keys --scp-upload -p 2222
|
|
696
812
|
"""
|
|
697
813
|
|
|
698
814
|
def build_parser(full=False):
|
|
@@ -718,6 +834,13 @@ def build_parser(full=False):
|
|
|
718
834
|
parser.add_argument("-1", "--single", action="store_true",
|
|
719
835
|
help=help_text(short_help="close the listener after first successful auth",
|
|
720
836
|
long_help="(and exit when that connection ends)"))
|
|
837
|
+
parser.add_argument("--mimic", metavar="PRESET", type=str.lower,
|
|
838
|
+
choices=list(MIMIC_PRESETS), default="none",
|
|
839
|
+
help=help_text(long_help="pose as another SSH server "
|
|
840
|
+
f"- presets (case-insensitive): {', '.join(list(MIMIC_PRESETS))} "
|
|
841
|
+
"- match the preset's pre-auth (banner, KEXINIT, server-sig-algs, ...) "
|
|
842
|
+
"exactly - banner can be overridden by --version-banner "
|
|
843
|
+
"- check Github repository for details"))
|
|
721
844
|
parser.add_argument("--host-key", metavar="FILE", type=Path,
|
|
722
845
|
help=help_text(long_help="server host key file, may hold several keys "
|
|
723
846
|
"- auto-generated if missing - uses ./sshcatch_host_key by default"))
|
|
@@ -752,9 +875,7 @@ def build_parser(full=False):
|
|
|
752
875
|
|
|
753
876
|
banners = parser.add_argument_group("banners")
|
|
754
877
|
banners.add_argument("--version-banner", metavar="STRING",
|
|
755
|
-
help=help_text(long_help="
|
|
756
|
-
"only first-glance deception, it can still be identified as asyncssh - "
|
|
757
|
-
f"presets (case-insensitive): {', '.join(VERSION_PRESETS)}"))
|
|
878
|
+
help=help_text(long_help="manually set 'SSH-2.0-STRING' version banner"))
|
|
758
879
|
banners.add_argument("--pre-auth-banner", metavar="STRING",
|
|
759
880
|
help=help_text(long_help="banner shown to every client before login"))
|
|
760
881
|
banners.add_argument("--post-auth-banner", metavar="STRING",
|
|
@@ -791,10 +912,9 @@ def main():
|
|
|
791
912
|
configure_logging(output=args.output, timestamps=args.timestamps,
|
|
792
913
|
plain=args.plain, console_level=console_level)
|
|
793
914
|
|
|
794
|
-
# Handle version-banner
|
|
795
|
-
if args.version_banner:
|
|
796
|
-
args.version_banner =
|
|
797
|
-
args.version_banner.lower(), args.version_banner)
|
|
915
|
+
# Handle --mimic version-banner
|
|
916
|
+
if args.mimic != "none" and not args.version_banner:
|
|
917
|
+
args.version_banner = MIMIC_PRESETS[args.mimic][0]
|
|
798
918
|
|
|
799
919
|
# Validate user format
|
|
800
920
|
if args.user:
|
|
@@ -815,6 +935,8 @@ def main():
|
|
|
815
935
|
try: asyncio.run(start_server(args))
|
|
816
936
|
except (OSError, ValueError) as e:
|
|
817
937
|
parser.error(f"Could not start server: {e}")
|
|
938
|
+
except RuntimeError as e:
|
|
939
|
+
parser.error(str(e))
|
|
818
940
|
except KeyboardInterrupt:
|
|
819
941
|
print()
|
|
820
942
|
|
|
File without changes
|
|
File without changes
|