From 7fc412d3b399bd4669c2ac5e0b27fab2b5aac048 Mon Sep 17 00:00:00 2001 From: Xavier Beaudouin Date: Mon, 7 Oct 2024 17:31:24 +0200 Subject: [PATCH] Backport SA + CVE --- .../openssh-portable/files/patch-9.8-cves | 56 +++ .../openssh-portable/files/patch-SA-23:19 | 425 ++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 security/openssh-portable/files/patch-9.8-cves create mode 100644 security/openssh-portable/files/patch-SA-23:19 diff --git a/security/openssh-portable/files/patch-9.8-cves b/security/openssh-portable/files/patch-9.8-cves new file mode 100644 index 0000000..2e47d58 --- /dev/null +++ b/security/openssh-portable/files/patch-9.8-cves @@ -0,0 +1,56 @@ +https://lists.mindrot.org/pipermail/openssh-unix-dev/2024-July/041431.html + +Damien Miller djm at mindrot.org +Mon Jul 1 18:21:11 AEST 2024 +Previous message (by thread): Announce: OpenSSH 9.8 released +Next message (by thread): Announce: OpenSSH 9.8 released +Messages sorted by: [ date ] [ thread ] [ subject ] [ author ] +Hi, + +Regarding the race condition fixed in OpenSSH 9.8. A mitigation to +prevent exploitation of this bug is to disable the login grace timer +by setting LoginGraceTime=0 in sshd_config. This will however make +it much easier for an attacker to deny service to sshd. + +Similarly, the much more minor keystroke timing bug can be avoided +by disabling the feature using ObscureKeystrokeTiming=0. + +Some users will understandably prefer to patch their OpenSSH rather +than upgrade to the newest version, so here are minimal patches for +both problems. + +1) Critical race condition in sshd + +2) Minor logic error in ObscureKeystrokeTiming + +--- log.c.orig 2024-07-02 09:05:35.023051000 -0700 ++++ log.c 2024-07-02 09:05:54.881067000 -0700 +@@ -451,12 +451,14 @@ sshsigdie(const char *file, const char *func, int line + sshsigdie(const char *file, const char *func, int line, int showfunc, + LogLevel level, const char *suffix, const char *fmt, ...) + { ++#ifdef SYSLOG_R_SAFE_IN_SIGHAND + va_list args; + + va_start(args, fmt); + sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL, + suffix, fmt, args); + va_end(args); ++#endif + _exit(1); + } + +--- clientloop.c.orig 2024-07-02 09:06:09.736347000 -0700 ++++ clientloop.c 2024-07-02 09:06:41.414979000 -0700 +@@ -608,8 +608,9 @@ obfuscate_keystroke_timing(struct ssh *ssh, struct tim + if (timespeccmp(&now, &chaff_until, >=)) { + /* Stop if there have been no keystrokes for a while */ + stop_reason = "chaff time expired"; +- } else if (timespeccmp(&now, &next_interval, >=)) { +- /* Otherwise if we were due to send, then send chaff */ ++ } else if (timespeccmp(&now, &next_interval, >=) && ++ !ssh_packet_have_data_to_write(ssh)) { ++ /* If due to send but have no data, then send chaff */ + if (send_chaff(ssh)) + nchaff++; + } diff --git a/security/openssh-portable/files/patch-SA-23:19 b/security/openssh-portable/files/patch-SA-23:19 new file mode 100644 index 0000000..6240578 --- /dev/null +++ b/security/openssh-portable/files/patch-SA-23:19 @@ -0,0 +1,425 @@ +--- kex.c.orig ++++ kex.c +@@ -65,7 +65,7 @@ + #include "xmalloc.h" + + /* prototype */ +-static int kex_choose_conf(struct ssh *); ++static int kex_choose_conf(struct ssh *, uint32_t seq); + static int kex_input_newkeys(int, u_int32_t, struct ssh *); + + static const char * const proposal_names[PROPOSAL_MAX] = { +@@ -177,6 +177,18 @@ + return 1; + } + ++/* returns non-zero if proposal contains any algorithm from algs */ ++static int ++has_any_alg(const char *proposal, const char *algs) ++{ ++ char *cp; ++ ++ if ((cp = match_list(proposal, algs, NULL)) == NULL) ++ return 0; ++ free(cp); ++ return 1; ++} ++ + /* + * Concatenate algorithm names, avoiding duplicates in the process. + * Caller must free returned string. +@@ -184,7 +196,7 @@ + char * + kex_names_cat(const char *a, const char *b) + { +- char *ret = NULL, *tmp = NULL, *cp, *p, *m; ++ char *ret = NULL, *tmp = NULL, *cp, *p; + size_t len; + + if (a == NULL || *a == '\0') +@@ -201,10 +213,8 @@ + } + strlcpy(ret, a, len); + for ((p = strsep(&cp, ",")); p && *p != '\0'; (p = strsep(&cp, ","))) { +- if ((m = match_list(ret, p, NULL)) != NULL) { +- free(m); ++ if (has_any_alg(ret, p)) + continue; /* Algorithm already present */ +- } + if (strlcat(ret, ",", len) >= len || + strlcat(ret, p, len) >= len) { + free(tmp); +@@ -334,15 +344,23 @@ + const char *defpropclient[PROPOSAL_MAX] = { KEX_CLIENT }; + const char **defprop = ssh->kex->server ? defpropserver : defpropclient; + u_int i; ++ char *cp; + + if (prop == NULL) + fatal_f("proposal missing"); + ++ /* Append EXT_INFO signalling to KexAlgorithms */ ++ if (kexalgos == NULL) ++ kexalgos = defprop[PROPOSAL_KEX_ALGS]; ++ if ((cp = kex_names_cat(kexalgos, ssh->kex->server ? ++ "kex-strict-s-v00@openssh.com" : ++ "ext-info-c,kex-strict-c-v00@openssh.com")) == NULL) ++ fatal_f("kex_names_cat"); ++ + for (i = 0; i < PROPOSAL_MAX; i++) { + switch(i) { + case PROPOSAL_KEX_ALGS: +- prop[i] = compat_kex_proposal(ssh, +- kexalgos ? kexalgos : defprop[i]); ++ prop[i] = compat_kex_proposal(ssh, cp); + break; + case PROPOSAL_ENC_ALGS_CTOS: + case PROPOSAL_ENC_ALGS_STOC: +@@ -363,6 +381,7 @@ + prop[i] = xstrdup(defprop[i]); + } + } ++ free(cp); + } + + void +@@ -466,7 +485,12 @@ + { + int r; + +- error("kex protocol error: type %d seq %u", type, seq); ++ /* If in strict mode, any unexpected message is an error */ ++ if ((ssh->kex->flags & KEX_INITIAL) && ssh->kex->kex_strict) { ++ ssh_packet_disconnect(ssh, "strict KEX violation: " ++ "unexpected packet type %u (seqnr %u)", type, seq); ++ } ++ error_f("type %u seq %u", type, seq); + if ((r = sshpkt_start(ssh, SSH2_MSG_UNIMPLEMENTED)) != 0 || + (r = sshpkt_put_u32(ssh, seq)) != 0 || + (r = sshpkt_send(ssh)) != 0) +@@ -563,7 +587,7 @@ + if (ninfo >= 1024) { + error("SSH2_MSG_EXT_INFO with too many entries, expected " + "<=1024, received %u", ninfo); +- return SSH_ERR_INVALID_FORMAT; ++ return dispatch_protocol_error(type, seq, ssh); + } + for (i = 0; i < ninfo; i++) { + if ((r = sshpkt_get_cstring(ssh, &name, NULL)) != 0) +@@ -681,7 +705,7 @@ + error_f("no kex"); + return SSH_ERR_INTERNAL_ERROR; + } +- ssh_dispatch_set(ssh, SSH2_MSG_KEXINIT, NULL); ++ ssh_dispatch_set(ssh, SSH2_MSG_KEXINIT, &kex_protocol_error); + ptr = sshpkt_ptr(ssh, &dlen); + if ((r = sshbuf_put(kex->peer, ptr, dlen)) != 0) + return r; +@@ -717,7 +741,7 @@ + if (!(kex->flags & KEX_INIT_SENT)) + if ((r = kex_send_kexinit(ssh)) != 0) + return r; +- if ((r = kex_choose_conf(ssh)) != 0) ++ if ((r = kex_choose_conf(ssh, seq)) != 0) + return r; + + if (kex->kex_type < KEX_MAX && kex->kex[kex->kex_type] != NULL) +@@ -981,20 +1005,14 @@ + return (1); + } + +-/* returns non-zero if proposal contains any algorithm from algs */ + static int +-has_any_alg(const char *proposal, const char *algs) ++kexalgs_contains(char **peer, const char *ext) + { +- char *cp; +- +- if ((cp = match_list(proposal, algs, NULL)) == NULL) +- return 0; +- free(cp); +- return 1; ++ return has_any_alg(peer[PROPOSAL_KEX_ALGS], ext); + } + + static int +-kex_choose_conf(struct ssh *ssh) ++kex_choose_conf(struct ssh *ssh, uint32_t seq) + { + struct kex *kex = ssh->kex; + struct newkeys *newkeys; +@@ -1019,13 +1037,23 @@ + sprop=peer; + } + +- /* Check whether client supports ext_info_c */ +- if (kex->server && (kex->flags & KEX_INITIAL)) { +- char *ext; +- +- ext = match_list("ext-info-c", peer[PROPOSAL_KEX_ALGS], NULL); +- kex->ext_info_c = (ext != NULL); +- free(ext); ++ /* Check whether peer supports ext_info/kex_strict */ ++ if ((kex->flags & KEX_INITIAL) != 0) { ++ if (kex->server) { ++ kex->ext_info_c = kexalgs_contains(peer, "ext-info-c"); ++ kex->kex_strict = kexalgs_contains(peer, ++ "kex-strict-c-v00@openssh.com"); ++ } else { ++ kex->kex_strict = kexalgs_contains(peer, ++ "kex-strict-s-v00@openssh.com"); ++ } ++ if (kex->kex_strict) { ++ debug3_f("will use strict KEX ordering"); ++ if (seq != 0) ++ ssh_packet_disconnect(ssh, ++ "strict KEX violation: " ++ "KEXINIT was not the first packet"); ++ } + } + + /* Check whether client supports rsa-sha2 algorithms */ +--- kex.h.orig ++++ kex.h +@@ -149,6 +149,7 @@ + u_int kex_type; + char *server_sig_algs; + int ext_info_c; ++ int kex_strict; + struct sshbuf *my; + struct sshbuf *peer; + struct sshbuf *client_version; +--- packet.c.orig ++++ packet.c +@@ -1208,8 +1208,13 @@ + sshbuf_dump(state->output, stderr); + #endif + /* increment sequence number for outgoing packets */ +- if (++state->p_send.seqnr == 0) ++ if (++state->p_send.seqnr == 0) { ++ if ((ssh->kex->flags & KEX_INITIAL) != 0) { ++ ssh_packet_disconnect(ssh, "outgoing sequence number " ++ "wrapped during initial key exchange"); ++ } + logit("outgoing seqnr wraps around"); ++ } + if (++state->p_send.packets == 0) + if (!(ssh->compat & SSH_BUG_NOREKEY)) + return SSH_ERR_NEED_REKEY; +@@ -1217,6 +1222,11 @@ + state->p_send.bytes += len; + sshbuf_reset(state->outgoing_packet); + ++ if (type == SSH2_MSG_NEWKEYS && ssh->kex->kex_strict) { ++ debug_f("resetting send seqnr %u", state->p_send.seqnr); ++ state->p_send.seqnr = 0; ++ } ++ + if (type == SSH2_MSG_NEWKEYS) + r = ssh_set_newkeys(ssh, MODE_OUT); + else if (type == SSH2_MSG_USERAUTH_SUCCESS && state->server_side) +@@ -1345,8 +1355,7 @@ + /* Stay in the loop until we have received a complete packet. */ + for (;;) { + /* Try to read a packet from the buffer. */ +- r = ssh_packet_read_poll_seqnr(ssh, typep, seqnr_p); +- if (r != 0) ++ if ((r = ssh_packet_read_poll_seqnr(ssh, typep, seqnr_p)) != 0) + break; + /* If we got a packet, return it. */ + if (*typep != SSH_MSG_NONE) +@@ -1417,29 +1426,6 @@ + return type; + } + +-/* +- * Waits until a packet has been received, verifies that its type matches +- * that given, and gives a fatal error and exits if there is a mismatch. +- */ +- +-int +-ssh_packet_read_expect(struct ssh *ssh, u_int expected_type) +-{ +- int r; +- u_char type; +- +- if ((r = ssh_packet_read_seqnr(ssh, &type, NULL)) != 0) +- return r; +- if (type != expected_type) { +- if ((r = sshpkt_disconnect(ssh, +- "Protocol error: expected packet type %d, got %d", +- expected_type, type)) != 0) +- return r; +- return SSH_ERR_PROTOCOL_ERROR; +- } +- return 0; +-} +- + static int + ssh_packet_read_poll2_mux(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) + { +@@ -1630,10 +1616,16 @@ + if ((r = sshbuf_consume(state->input, mac->mac_len)) != 0) + goto out; + } ++ + if (seqnr_p != NULL) + *seqnr_p = state->p_read.seqnr; +- if (++state->p_read.seqnr == 0) ++ if (++state->p_read.seqnr == 0) { ++ if ((ssh->kex->flags & KEX_INITIAL) != 0) { ++ ssh_packet_disconnect(ssh, "incoming sequence number " ++ "wrapped during initial key exchange"); ++ } + logit("incoming seqnr wraps around"); ++ } + if (++state->p_read.packets == 0) + if (!(ssh->compat & SSH_BUG_NOREKEY)) + return SSH_ERR_NEED_REKEY; +@@ -1699,6 +1691,10 @@ + #endif + /* reset for next packet */ + state->packlen = 0; ++ if (*typep == SSH2_MSG_NEWKEYS && ssh->kex->kex_strict) { ++ debug_f("resetting read seqnr %u", state->p_read.seqnr); ++ state->p_read.seqnr = 0; ++ } + + if ((r = ssh_packet_check_rekey(ssh)) != 0) + return r; +@@ -1721,10 +1717,39 @@ + r = ssh_packet_read_poll2(ssh, typep, seqnr_p); + if (r != 0) + return r; +- if (*typep) { +- state->keep_alive_timeouts = 0; +- DBG(debug("received packet type %d", *typep)); ++ if (*typep == 0) { ++ /* no message ready */ ++ return 0; + } ++ state->keep_alive_timeouts = 0; ++ DBG(debug("received packet type %d", *typep)); ++ ++ /* Always process disconnect messages */ ++ if (*typep == SSH2_MSG_DISCONNECT) { ++ if ((r = sshpkt_get_u32(ssh, &reason)) != 0 || ++ (r = sshpkt_get_string(ssh, &msg, NULL)) != 0) ++ return r; ++ /* Ignore normal client exit notifications */ ++ do_log2(ssh->state->server_side && ++ reason == SSH2_DISCONNECT_BY_APPLICATION ? ++ SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR, ++ "Received disconnect from %s port %d:" ++ "%u: %.400s", ssh_remote_ipaddr(ssh), ++ ssh_remote_port(ssh), reason, msg); ++ free(msg); ++ return SSH_ERR_DISCONNECTED; ++ } ++ ++ /* ++ * Do not implicitly handle any messages here during initial ++ * KEX when in strict mode. They will be need to be allowed ++ * explicitly by the KEX dispatch table or they will generate ++ * protocol errors. ++ */ ++ if (ssh->kex != NULL && ++ (ssh->kex->flags & KEX_INITIAL) && ssh->kex->kex_strict) ++ return 0; ++ /* Implicitly handle transport-level messages */ + switch (*typep) { + case SSH2_MSG_IGNORE: + debug3("Received SSH2_MSG_IGNORE"); +@@ -1739,19 +1764,6 @@ + debug("Remote: %.900s", msg); + free(msg); + break; +- case SSH2_MSG_DISCONNECT: +- if ((r = sshpkt_get_u32(ssh, &reason)) != 0 || +- (r = sshpkt_get_string(ssh, &msg, NULL)) != 0) +- return r; +- /* Ignore normal client exit notifications */ +- do_log2(ssh->state->server_side && +- reason == SSH2_DISCONNECT_BY_APPLICATION ? +- SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR, +- "Received disconnect from %s port %d:" +- "%u: %.400s", ssh_remote_ipaddr(ssh), +- ssh_remote_port(ssh), reason, msg); +- free(msg); +- return SSH_ERR_DISCONNECTED; + case SSH2_MSG_UNIMPLEMENTED: + if ((r = sshpkt_get_u32(ssh, &seqnr)) != 0) + return r; +@@ -2244,6 +2256,7 @@ + (r = sshbuf_put_u32(m, kex->hostkey_type)) != 0 || + (r = sshbuf_put_u32(m, kex->hostkey_nid)) != 0 || + (r = sshbuf_put_u32(m, kex->kex_type)) != 0 || ++ (r = sshbuf_put_u32(m, kex->kex_strict)) != 0 || + (r = sshbuf_put_stringb(m, kex->my)) != 0 || + (r = sshbuf_put_stringb(m, kex->peer)) != 0 || + (r = sshbuf_put_stringb(m, kex->client_version)) != 0 || +@@ -2406,6 +2419,7 @@ + (r = sshbuf_get_u32(m, (u_int *)&kex->hostkey_type)) != 0 || + (r = sshbuf_get_u32(m, (u_int *)&kex->hostkey_nid)) != 0 || + (r = sshbuf_get_u32(m, &kex->kex_type)) != 0 || ++ (r = sshbuf_get_u32(m, &kex->kex_strict)) != 0 || + (r = sshbuf_get_stringb(m, kex->my)) != 0 || + (r = sshbuf_get_stringb(m, kex->peer)) != 0 || + (r = sshbuf_get_stringb(m, kex->client_version)) != 0 || +@@ -2734,6 +2748,7 @@ + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + ++ debug2_f("sending SSH2_MSG_DISCONNECT: %s", buf); + if ((r = sshpkt_start(ssh, SSH2_MSG_DISCONNECT)) != 0 || + (r = sshpkt_put_u32(ssh, SSH2_DISCONNECT_PROTOCOL_ERROR)) != 0 || + (r = sshpkt_put_cstring(ssh, buf)) != 0 || +--- packet.h.orig ++++ packet.h +@@ -124,7 +124,6 @@ + int ssh_packet_send2(struct ssh *); + + int ssh_packet_read(struct ssh *); +-int ssh_packet_read_expect(struct ssh *, u_int type); + int ssh_packet_read_poll(struct ssh *); + int ssh_packet_read_poll2(struct ssh *, u_char *, u_int32_t *seqnr_p); + int ssh_packet_process_incoming(struct ssh *, const char *buf, u_int len); +--- sshconnect2.c.orig ++++ sshconnect2.c +@@ -358,7 +358,6 @@ + }; + + static int input_userauth_service_accept(int, u_int32_t, struct ssh *); +-static int input_userauth_ext_info(int, u_int32_t, struct ssh *); + static int input_userauth_success(int, u_int32_t, struct ssh *); + static int input_userauth_failure(int, u_int32_t, struct ssh *); + static int input_userauth_banner(int, u_int32_t, struct ssh *); +@@ -472,7 +471,7 @@ + + ssh->authctxt = &authctxt; + ssh_dispatch_init(ssh, &input_userauth_error); +- ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, &input_userauth_ext_info); ++ ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, kex_input_ext_info); + ssh_dispatch_set(ssh, SSH2_MSG_SERVICE_ACCEPT, &input_userauth_service_accept); + ssh_dispatch_run_fatal(ssh, DISPATCH_BLOCK, &authctxt.success); /* loop until success */ + pubkey_cleanup(ssh); +@@ -523,12 +522,6 @@ + return r; + } + +-static int +-input_userauth_ext_info(int type, u_int32_t seqnr, struct ssh *ssh) +-{ +- return kex_input_ext_info(type, seqnr, ssh); +-} +- + void + userauth(struct ssh *ssh, char *authlist) + { +@@ -607,6 +600,7 @@ + free(authctxt->methoddata); + authctxt->methoddata = NULL; + authctxt->success = 1; /* break out */ ++ ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, dispatch_protocol_error); + return 0; + }