Skip to content

Commit fad4e1f

Browse files
authored
Merge pull request #258 from Castaglia/proxy-sftp-terrapin-issue257
Issue #257: Implement support for the Terrapin attack "strict KEX" mi…
2 parents f3b83fe + 5461273 commit fad4e1f

File tree

7 files changed

+124
-21
lines changed

7 files changed

+124
-21
lines changed

include/proxy/ssh.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* ProFTPD - mod_proxy SSH API
3-
* Copyright (c) 2021 TJ Saunders
3+
* Copyright (c) 2021-2023 TJ Saunders
44
*
55
* This program is free software; you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -37,6 +37,7 @@
3737
#define PROXY_OPT_SSH_ALLOW_WEAK_SECURITY 0x0800
3838
#define PROXY_OPT_SSH_NO_EXT_INFO 0x1000
3939
#define PROXY_OPT_SSH_NO_HOSTKEY_ROTATION 0x2000
40+
#define PROXY_OPT_SSH_NO_STRICT_KEX 0x4000
4041

4142
int proxy_ssh_init(pool *p, const char *tables_dir, int flags);
4243
int proxy_ssh_free(pool *p);

include/proxy/ssh/packet.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* ProFTPD - mod_proxy SSH packet API
3-
* Copyright (c) 2021 TJ Saunders
3+
* Copyright (c) 2021-2023 TJ Saunders
44
*
55
* This program is free software; you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -132,6 +132,13 @@ void proxy_ssh_packet_handle_ext_info(struct proxy_ssh_packet *pkt);
132132
void proxy_ssh_packet_handle_ignore(struct proxy_ssh_packet *pkt);
133133
void proxy_ssh_packet_handle_unimplemented(struct proxy_ssh_packet *pkt);
134134

135+
/* These are used for implementing the "strict KEX" mitigations of the Terrapin
136+
* attack (Issue 257).
137+
*/
138+
uint32_t proxy_ssh_packet_get_server_seqno(void);
139+
void proxy_ssh_packet_reset_client_seqno(void);
140+
void proxy_ssh_packet_reset_server_seqno(void);
141+
135142
int proxy_ssh_packet_set_version(const char *client_version);
136143
int proxy_ssh_packet_send_version(conn_t *conn);
137144

lib/proxy/ssh/kex.c

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* ProFTPD - mod_proxy SSH key exchange (kex)
3-
* Copyright (c) 2021-2022 TJ Saunders
3+
* Copyright (c) 2021-2023 TJ Saunders
44
*
55
* This program is free software; you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -183,6 +183,13 @@ static struct proxy_ssh_kex *kex_first_kex = NULL;
183183
static struct proxy_ssh_kex *kex_rekey_kex = NULL;
184184
static int kex_sent_kexinit = FALSE;
185185

186+
/* Using strict kex? Note that we maintain this value here, rather than
187+
* in the proxy_ssh_kex struct, so that any "use strict KEX" flag set via the
188+
* first KEXINIT is used through any subsequent KEXINITs.
189+
*/
190+
static int use_strict_kex = FALSE;
191+
static int kex_done_first_kex = FALSE;
192+
186193
/* Diffie-Hellman group moduli */
187194

188195
static const char *dh_group1_str =
@@ -1660,6 +1667,16 @@ static const char *get_kexinit_exchange_list(pool *p) {
16601667
res = pstrcat(p, res, *res ? "," : "", pstrdup(p, "ext-info-c"), NULL);
16611668
}
16621669

1670+
if (!(proxy_opts & PROXY_OPT_SSH_NO_STRICT_KEX)) {
1671+
/* Indicate support for OpenSSH's custom "strict KEX" mode extension,
1672+
* but only if we have not done/completed our first KEX.
1673+
*/
1674+
if (kex_done_first_kex == FALSE) {
1675+
res = pstrcat(p, res, *res ? "," : "",
1676+
pstrdup(p, "kex-strict-c-v00@openssh.com"), NULL);
1677+
}
1678+
}
1679+
16631680
return res;
16641681
}
16651682

@@ -2271,6 +2288,21 @@ static int get_session_names(struct proxy_ssh_kex *kex, int *correct_guess) {
22712288
pr_trace_msg(trace_channel, 20, "server %s EXT_INFO support",
22722289
kex->use_ext_info ? "signaled" : "did not signal" );
22732290

2291+
if (!(proxy_opts & PROXY_OPT_SSH_NO_STRICT_KEX)) {
2292+
/* Did the server indicate "strict kex" support (Issue 257)?
2293+
*
2294+
* Note that we only check for this if it is our first KEXINIT.
2295+
* The "strict kex" extension is ignored in any subsequent KEXINITs, as
2296+
* for rekeys.
2297+
*/
2298+
if (kex_done_first_kex == FALSE) {
2299+
use_strict_kex = proxy_ssh_misc_namelist_contains(kex->pool,
2300+
server_list, "kex-strict-s-v00@openssh.com");
2301+
pr_trace_msg(trace_channel, 20, "server %s strict KEX support",
2302+
use_strict_kex ? "signaled" : "did not signal" );
2303+
}
2304+
}
2305+
22742306
} else {
22752307
(void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION,
22762308
"no shared key exchange algorithm found (client sent '%s', server sent "
@@ -3018,6 +3050,10 @@ static struct proxy_ssh_packet *read_kex_packet(pool *p,
30183050
/* Per RFC 4253, Section 11, DEBUG, DISCONNECT, IGNORE, and UNIMPLEMENTED
30193051
* messages can occur at any time, even during KEX. We have to be prepared
30203052
* for this, and Do The Right Thing(tm).
3053+
*
3054+
* However, due to the Terrapin attack, if we are using a "strict KEX"
3055+
* mode, then only DISCONNECT messages can occur during KEX; DEBUG,
3056+
* IGNORE, and UNIMPLEMENTED messages will lead to disconnecting.
30213057
*/
30223058

30233059
msg_type = proxy_ssh_packet_get_msg_type(pkt);
@@ -3046,35 +3082,43 @@ static struct proxy_ssh_packet *read_kex_packet(pool *p,
30463082
}
30473083

30483084
switch (msg_type) {
3049-
case PROXY_SSH_MSG_DEBUG:
3050-
proxy_ssh_packet_handle_debug(pkt);
3051-
pr_response_set_pool(NULL);
3052-
pkt = NULL;
3053-
break;
3054-
3085+
/* DISCONNECT messages are always allowed. */
30553086
case PROXY_SSH_MSG_DISCONNECT:
30563087
proxy_ssh_packet_handle_disconnect(pkt);
30573088
pr_response_set_pool(NULL);
30583089
pkt = NULL;
30593090
break;
30603091

3092+
case PROXY_SSH_MSG_DEBUG:
3093+
if (use_strict_kex == FALSE) {
3094+
proxy_ssh_packet_handle_debug(pkt);
3095+
pr_response_set_pool(NULL);
3096+
pkt = NULL;
3097+
break;
3098+
}
3099+
30613100
case PROXY_SSH_MSG_IGNORE:
3062-
proxy_ssh_packet_handle_ignore(pkt);
3063-
pr_response_set_pool(NULL);
3064-
pkt = NULL;
3065-
break;
3101+
if (use_strict_kex == FALSE) {
3102+
proxy_ssh_packet_handle_ignore(pkt);
3103+
pr_response_set_pool(NULL);
3104+
pkt = NULL;
3105+
break;
3106+
}
30663107

30673108
case PROXY_SSH_MSG_UNIMPLEMENTED:
3068-
proxy_ssh_packet_handle_unimplemented(pkt);
3069-
pr_response_set_pool(NULL);
3070-
pkt = NULL;
3071-
break;
3109+
if (use_strict_kex == FALSE) {
3110+
proxy_ssh_packet_handle_unimplemented(pkt);
3111+
pr_response_set_pool(NULL);
3112+
pkt = NULL;
3113+
break;
3114+
}
30723115

30733116
default:
30743117
/* For any other message type, it's considered a protocol error. */
30753118
(void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION,
3076-
"received %s (%d) unexpectedly, disconnecting",
3077-
proxy_ssh_packet_get_msg_type_desc(msg_type), msg_type);
3119+
"received %s (%d) unexpectedly%s, disconnecting",
3120+
proxy_ssh_packet_get_msg_type_desc(msg_type), msg_type,
3121+
use_strict_kex ? " during strict KEX" : "");
30783122
pr_response_set_pool(NULL);
30793123
destroy_kex(kex);
30803124
destroy_pool(pkt->pool);
@@ -4903,6 +4947,25 @@ int proxy_ssh_kex_handle(struct proxy_ssh_packet *pkt,
49034947
return -1;
49044948
}
49054949

4950+
if (use_strict_kex == TRUE &&
4951+
kex_done_first_kex == FALSE) {
4952+
uint32_t server_seqno;
4953+
4954+
server_seqno = proxy_ssh_packet_get_server_seqno();
4955+
if (server_seqno != 1) {
4956+
/* Receiving any messages other than a KEXINIT as the first server
4957+
* message indicates the possibility of the Terrapin attack being
4958+
* conducted (Issue 257). Thus we disconnect the server in such
4959+
* cases.
4960+
*/
4961+
(void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION,
4962+
"'strict KEX' violation, as KEXINIT was not the first message; disconnecting");
4963+
destroy_kex(kex);
4964+
PROXY_SSH_DISCONNECT_CONN(proxy_sess->backend_ctrl_conn,
4965+
PROXY_SSH_DISCONNECT_BY_APPLICATION, NULL);
4966+
}
4967+
}
4968+
49064969
/* Once we have received the server KEXINIT message, we can compare what we
49074970
* want to send against what we already received from the server.
49084971
*
@@ -5063,6 +5126,11 @@ int proxy_ssh_kex_handle(struct proxy_ssh_packet *pkt,
50635126
sent_newkeys = TRUE;
50645127
}
50655128

5129+
if (use_strict_kex == TRUE) {
5130+
proxy_ssh_packet_reset_client_seqno();
5131+
proxy_ssh_packet_reset_server_seqno();
5132+
}
5133+
50665134
/* Last but certainly not least, set up the keys for encryption and
50675135
* authentication, based on H and K.
50685136
*/
@@ -5075,6 +5143,9 @@ int proxy_ssh_kex_handle(struct proxy_ssh_packet *pkt,
50755143
PROXY_SSH_DISCONNECT_BY_APPLICATION, NULL);
50765144
}
50775145

5146+
/* We've now completed our KEX, possibly our first. */
5147+
kex_done_first_kex = TRUE;
5148+
50785149
destroy_pool(pkt->pool);
50795150
destroy_kex(kex);
50805151
return 0;

lib/proxy/ssh/packet.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2325,6 +2325,18 @@ int proxy_ssh_packet_send_version(conn_t *conn) {
23252325
return 0;
23262326
}
23272327

2328+
uint32_t proxy_ssh_packet_get_server_seqno(void) {
2329+
return packet_server_seqno;
2330+
}
2331+
2332+
void proxy_ssh_packet_reset_client_seqno(void) {
2333+
packet_client_seqno = 0;
2334+
}
2335+
2336+
void proxy_ssh_packet_reset_server_seqno(void) {
2337+
packet_server_seqno = 0;
2338+
}
2339+
23282340
int proxy_ssh_packet_set_version(const char *version) {
23292341
if (client_version == NULL) {
23302342
errno = EINVAL;

mod_proxy.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,9 @@ MODRET set_proxysftpoptions(cmd_rec *cmd) {
12421242
} else if (strcmp(cmd->argv[i], "NoHostkeyRotation") == 0) {
12431243
opts |= PROXY_OPT_SSH_NO_HOSTKEY_ROTATION;
12441244

1245+
} else if (strcmp(cmd->argv[i], "NoStrictKex") == 0) {
1246+
opts |= PROXY_OPT_SSH_NO_STRICT_KEX;
1247+
12451248
} else {
12461249
CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown ProxySFTPOption '",
12471250
cmd->argv[i], "'", NULL));

mod_proxy.h.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* ProFTPD - mod_proxy
3-
* Copyright (c) 2012-2022 TJ Saunders
3+
* Copyright (c) 2012-2023 TJ Saunders
44
*
55
* This program is free software; you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -88,7 +88,7 @@
8888
/* Define if you have the strnstr(3) function. */
8989
#undef HAVE_STRNSTR
9090

91-
#define MOD_PROXY_VERSION "mod_proxy/0.9.2"
91+
#define MOD_PROXY_VERSION "mod_proxy/0.9.3"
9292

9393
/* Make sure the version of proftpd is as necessary. */
9494
#if PROFTPD_VERSION_NUMBER < 0x0001030706

mod_proxy.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,15 @@ <h3><a name="ProxySFTPOptions">ProxySFTPOptions</a></h3>
10911091
these custom OpenSSH extensions.
10921092
</li>
10931093

1094+
<p>
1095+
<li><code>NoStrictKex</code><br>
1096+
<p>
1097+
By default, <code>mod_proxy</code> will honor/support the OpenSSH
1098+
"strict KEX" mode extension, "kex-strict-c-v00@openssh.com" and
1099+
"kex-strict-s-v00@openssh.com". Use this option to disable support for
1100+
these custom OpenSSH extensions.
1101+
</li>
1102+
10941103
<p>
10951104
<li><code>OldProtocolCompat</code><br>
10961105
<p>

0 commit comments

Comments
 (0)