diff --git a/Recipe b/Recipe index b735ac47..a2728c08 100644 --- a/Recipe +++ b/Recipe @@ -251,7 +251,7 @@ NONSSH = telnet raw rlogin ldisc pinger # SSH back end (putty, plink, pscp, psftp). SSH = ssh ssh1bpp ssh2bpp ssh2bpp-bare ssh1censor ssh2censor - + sshcrc sshdes sshmd5 sshrsa sshrand sshsha sshblowf + + sshverstring sshcrc sshdes sshmd5 sshrsa sshrand sshsha sshblowf + sshdh sshcrcda sshpubk sshzlib sshdss x11fwd portfwd + sshaes sshccp sshsh256 sshsh512 sshbn wildcard pinger ssharcf + sshgssc pgssapi sshshare sshecc aqsync marshal nullplug agentf diff --git a/ssh.c b/ssh.c index 7031a554..9ac60d27 100644 --- a/ssh.c +++ b/ssh.c @@ -661,6 +661,7 @@ enum RekeyClass { struct ssh_tag { char *v_c, *v_s; + struct ssh_version_receiver version_receiver; ssh_hash *exhash; Socket s; @@ -795,7 +796,6 @@ struct ssh_tag { BinaryPacketProtocol *bpp; void (*general_packet_processing)(Ssh ssh, PktIn *pkt); - void (*current_incoming_data_fn) (Ssh ssh); void (*current_user_input_fn) (Ssh ssh); /* @@ -1149,26 +1149,6 @@ static void ssh_pkt_write(Ssh ssh, PktOut *pkt) queue_idempotent_callback(&ssh->outgoing_data_sender); } -static int ssh_versioncmp(const char *a, const char *b) -{ - char *ae, *be; - unsigned long av, bv; - - av = strtoul(a, &ae, 10); - bv = strtoul(b, &be, 10); - if (av != bv) - return (av < bv ? -1 : +1); - if (*ae == '.') - ae++; - if (*be == '.') - be++; - av = strtoul(ae, &ae, 10); - bv = strtoul(be, &be, 10); - if (av != bv) - return (av < bv ? -1 : +1); - return 0; -} - /* * Packet construction functions. Mostly shared between SSH-1 and SSH-2. */ @@ -1346,258 +1326,6 @@ static void ssh2_add_sigblob(Ssh ssh, PktOut *pkt, put_string(pkt, sigblob, sigblob_len); } -/* - * Examine the remote side's version string and compare it against - * a list of known buggy implementations. - */ -static void ssh_detect_bugs(Ssh ssh, char *vstring) -{ - char *imp; /* pointer to implementation part */ - imp = vstring; - imp += strcspn(imp, "-"); - if (*imp) imp++; - imp += strcspn(imp, "-"); - if (*imp) imp++; - - ssh->remote_bugs = 0; - - /* - * General notes on server version strings: - * - Not all servers reporting "Cisco-1.25" have all the bugs listed - * here -- in particular, we've heard of one that's perfectly happy - * with SSH1_MSG_IGNOREs -- but this string never seems to change, - * so we can't distinguish them. - */ - if (conf_get_int(ssh->conf, CONF_sshbug_ignore1) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_ignore1) == AUTO && - (!strcmp(imp, "1.2.18") || !strcmp(imp, "1.2.19") || - !strcmp(imp, "1.2.20") || !strcmp(imp, "1.2.21") || - !strcmp(imp, "1.2.22") || !strcmp(imp, "Cisco-1.25") || - !strcmp(imp, "OSU_1.4alpha3") || !strcmp(imp, "OSU_1.5alpha4")))) { - /* - * These versions don't support SSH1_MSG_IGNORE, so we have - * to use a different defence against password length - * sniffing. - */ - ssh->remote_bugs |= BUG_CHOKES_ON_SSH1_IGNORE; - logevent("We believe remote version has SSH-1 ignore bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_plainpw1) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_plainpw1) == AUTO && - (!strcmp(imp, "Cisco-1.25") || !strcmp(imp, "OSU_1.4alpha3")))) { - /* - * These versions need a plain password sent; they can't - * handle having a null and a random length of data after - * the password. - */ - ssh->remote_bugs |= BUG_NEEDS_SSH1_PLAIN_PASSWORD; - logevent("We believe remote version needs a plain SSH-1 password"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_rsa1) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_rsa1) == AUTO && - (!strcmp(imp, "Cisco-1.25")))) { - /* - * These versions apparently have no clue whatever about - * RSA authentication and will panic and die if they see - * an AUTH_RSA message. - */ - ssh->remote_bugs |= BUG_CHOKES_ON_RSA; - logevent("We believe remote version can't handle SSH-1 RSA authentication"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_hmac2) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_hmac2) == AUTO && - !wc_match("* VShell", imp) && - (wc_match("2.1.0*", imp) || wc_match("2.0.*", imp) || - wc_match("2.2.0*", imp) || wc_match("2.3.0*", imp) || - wc_match("2.1 *", imp)))) { - /* - * These versions have the HMAC bug. - */ - ssh->remote_bugs |= BUG_SSH2_HMAC; - logevent("We believe remote version has SSH-2 HMAC bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_derivekey2) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_derivekey2) == AUTO && - !wc_match("* VShell", imp) && - (wc_match("2.0.0*", imp) || wc_match("2.0.10*", imp) ))) { - /* - * These versions have the key-derivation bug (failing to - * include the literal shared secret in the hashes that - * generate the keys). - */ - ssh->remote_bugs |= BUG_SSH2_DERIVEKEY; - logevent("We believe remote version has SSH-2 key-derivation bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_rsapad2) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_rsapad2) == AUTO && - (wc_match("OpenSSH_2.[5-9]*", imp) || - wc_match("OpenSSH_3.[0-2]*", imp) || - wc_match("mod_sftp/0.[0-8]*", imp) || - wc_match("mod_sftp/0.9.[0-8]", imp)))) { - /* - * These versions have the SSH-2 RSA padding bug. - */ - ssh->remote_bugs |= BUG_SSH2_RSA_PADDING; - logevent("We believe remote version has SSH-2 RSA padding bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_pksessid2) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_pksessid2) == AUTO && - wc_match("OpenSSH_2.[0-2]*", imp))) { - /* - * These versions have the SSH-2 session-ID bug in - * public-key authentication. - */ - ssh->remote_bugs |= BUG_SSH2_PK_SESSIONID; - logevent("We believe remote version has SSH-2 public-key-session-ID bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_rekey2) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_rekey2) == AUTO && - (wc_match("DigiSSH_2.0", imp) || - wc_match("OpenSSH_2.[0-4]*", imp) || - wc_match("OpenSSH_2.5.[0-3]*", imp) || - wc_match("Sun_SSH_1.0", imp) || - wc_match("Sun_SSH_1.0.1", imp) || - /* All versions <= 1.2.6 (they changed their format in 1.2.7) */ - wc_match("WeOnlyDo-*", imp)))) { - /* - * These versions have the SSH-2 rekey bug. - */ - ssh->remote_bugs |= BUG_SSH2_REKEY; - logevent("We believe remote version has SSH-2 rekey bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_maxpkt2) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_maxpkt2) == AUTO && - (wc_match("1.36_sshlib GlobalSCAPE", imp) || - wc_match("1.36 sshlib: GlobalScape", imp)))) { - /* - * This version ignores our makpkt and needs to be throttled. - */ - ssh->remote_bugs |= BUG_SSH2_MAXPKT; - logevent("We believe remote version ignores SSH-2 maximum packet size"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_ignore2) == FORCE_ON) { - /* - * Servers that don't support SSH2_MSG_IGNORE. Currently, - * none detected automatically. - */ - ssh->remote_bugs |= BUG_CHOKES_ON_SSH2_IGNORE; - logevent("We believe remote version has SSH-2 ignore bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_oldgex2) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_oldgex2) == AUTO && - (wc_match("OpenSSH_2.[235]*", imp)))) { - /* - * These versions only support the original (pre-RFC4419) - * SSH-2 GEX request, and disconnect with a protocol error if - * we use the newer version. - */ - ssh->remote_bugs |= BUG_SSH2_OLDGEX; - logevent("We believe remote version has outdated SSH-2 GEX"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_winadj) == FORCE_ON) { - /* - * Servers that don't support our winadj request for one - * reason or another. Currently, none detected automatically. - */ - ssh->remote_bugs |= BUG_CHOKES_ON_WINADJ; - logevent("We believe remote version has winadj bug"); - } - - if (conf_get_int(ssh->conf, CONF_sshbug_chanreq) == FORCE_ON || - (conf_get_int(ssh->conf, CONF_sshbug_chanreq) == AUTO && - (wc_match("OpenSSH_[2-5].*", imp) || - wc_match("OpenSSH_6.[0-6]*", imp) || - wc_match("dropbear_0.[2-4][0-9]*", imp) || - wc_match("dropbear_0.5[01]*", imp)))) { - /* - * These versions have the SSH-2 channel request bug. - * OpenSSH 6.7 and above do not: - * https://bugzilla.mindrot.org/show_bug.cgi?id=1818 - * dropbear_0.52 and above do not: - * https://secure.ucc.asn.au/hg/dropbear/rev/cd02449b709c - */ - ssh->remote_bugs |= BUG_SENDS_LATE_REQUEST_REPLY; - logevent("We believe remote version has SSH-2 channel request bug"); - } -} - -/* - * The `software version' part of an SSH version string is required - * to contain no spaces or minus signs. - */ -static void ssh_fix_verstring(char *str) -{ - /* Eat "-". */ - while (*str && *str != '-') str++; - assert(*str == '-'); str++; - - /* Convert minus signs and spaces in the remaining string into - * underscores. */ - while (*str) { - if (*str == '-' || *str == ' ') - *str = '_'; - str++; - } -} - -/* - * Send an appropriate SSH version string. - */ -static void ssh_send_verstring(Ssh ssh, const char *protoname, char *svers) -{ - char *verstring; - - if (ssh->version == 2) { - /* - * Construct a v2 version string. - */ - verstring = dupprintf("%s2.0-%s\015\012", protoname, sshver); - } else { - /* - * Construct a v1 version string. - */ - assert(!strcmp(protoname, "SSH-")); /* no v1 bare connection protocol */ - verstring = dupprintf("SSH-%s-%s\012", - (ssh_versioncmp(svers, "1.5") <= 0 ? - svers : "1.5"), - sshver); - } - - ssh_fix_verstring(verstring + strlen(protoname)); -#ifdef FUZZING - /* FUZZING make PuTTY insecure, so make live use difficult. */ - verstring[0] = 'I'; -#endif - - if (ssh->version == 2) { - size_t len; - /* - * Record our version string. - */ - len = strcspn(verstring, "\015\012"); - ssh->v_c = snewn(len + 1, char); - memcpy(ssh->v_c, verstring, len); - ssh->v_c[len] = 0; - } - - logeventf(ssh, "We claim version: %.*s", - strcspn(verstring, "\015\012"), verstring); - bufchain_add(&ssh->outgoing_data, verstring, strlen(verstring)); - queue_idempotent_callback(&ssh->outgoing_data_sender); - sfree(verstring); -} - static void ssh_feed_to_bpp(Ssh ssh) { PacketQueueNode *prev_tail = ssh->pq_full.end.prev; @@ -1628,342 +1356,83 @@ static void ssh_feed_to_bpp(Ssh ssh) queue_idempotent_callback(&ssh->pq_full_consumer); } -static void do_ssh_init(Ssh ssh) +static void ssh_got_ssh_version(struct ssh_version_receiver *rcv, + int major_version) { - static const char protoname[] = "SSH-"; - - struct do_ssh_init_state { - int crLine; - int vslen; - char *vstring; - char *version; - int vstrsize; - int i; - int proto1, proto2; - }; - crState(do_ssh_init_state); - - crBeginState; + Ssh ssh = FROMFIELD(rcv, struct ssh_tag, version_receiver); + BinaryPacketProtocol *old_bpp; /* - * Search for a line beginning with the protocol name prefix in - * the input. + * Queue an outgoing-data run: if the version string has been sent + * late rather than early, it'll still be sitting on our output + * raw data queue. */ - s->i = 0; - while (1) { - char prefix[sizeof(protoname)-1]; - - /* - * Every time round this loop, we're at the start of a new - * line, so look for the prefix. - */ - crMaybeWaitUntilV( - bufchain_size(&ssh->incoming_data) >= sizeof(prefix)); - bufchain_fetch(&ssh->incoming_data, prefix, sizeof(prefix)); - if (!memcmp(prefix, protoname, sizeof(prefix))) { - bufchain_consume(&ssh->incoming_data, sizeof(prefix)); - break; - } - - /* - * If we didn't find it, consume data until we see a newline. - */ - while (1) { - int len; - void *data; - char *nl; - - crMaybeWaitUntilV(bufchain_size(&ssh->incoming_data) > 0); - bufchain_prefix(&ssh->incoming_data, &data, &len); - if ((nl = memchr(data, '\012', len)) != NULL) { - bufchain_consume(&ssh->incoming_data, nl - (char *)data + 1); - break; - } else { - bufchain_consume(&ssh->incoming_data, len); - } - } - } - - ssh->session_started = TRUE; - ssh->agentfwd_enabled = FALSE; + queue_idempotent_callback(&ssh->outgoing_data_sender); /* - * Now read the rest of the greeting line. + * We don't support choosing a major protocol version dynamically, + * so this should always be the same value we set up in + * connect_to_host(). */ - s->vstrsize = sizeof(protoname) + 16; - s->vstring = snewn(s->vstrsize, char); - strcpy(s->vstring, protoname); - s->vslen = strlen(protoname); - s->i = 0; - do { - int len; - void *data; - char *nl; + assert(ssh->version == major_version); - crMaybeWaitUntilV(bufchain_size(&ssh->incoming_data) > 0); - bufchain_prefix(&ssh->incoming_data, &data, &len); - if ((nl = memchr(data, '\012', len)) != NULL) { - len = nl - (char *)data + 1; + old_bpp = ssh->bpp; + ssh->remote_bugs = ssh_verstring_get_bugs(old_bpp); + + if (!ssh->bare_connection) { + if (ssh->version == 2) { + /* + * Retrieve both version strings from the old BPP before + * we free it. + */ + ssh->v_s = dupstr(ssh_verstring_get_remote(old_bpp)); + ssh->v_c = dupstr(ssh_verstring_get_local(old_bpp)); + + /* + * Initialise SSH-2 protocol. + */ + ssh2_protocol_setup(ssh); + ssh->general_packet_processing = ssh2_general_packet_processing; + ssh->current_user_input_fn = NULL; + } else { + /* + * Initialise SSH-1 protocol. + */ + ssh1_protocol_setup(ssh); + ssh->current_user_input_fn = ssh1_login_input; } - if (s->vslen + len >= s->vstrsize - 1) { - s->vstrsize = (s->vslen + len) * 5 / 4 + 32; - s->vstring = sresize(s->vstring, s->vstrsize, char); - } + if (ssh->version == 2) + queue_idempotent_callback(&ssh->ssh2_transport_icb); - memcpy(s->vstring + s->vslen, data, len); - s->vslen += len; - bufchain_consume(&ssh->incoming_data, len); - - } while (s->vstring[s->vslen-1] != '\012'); - - s->vstring[s->vslen] = 0; - s->vstring[strcspn(s->vstring, "\015\012")] = '\0';/* remove EOL chars */ - - logeventf(ssh, "Server version: %s", s->vstring); - ssh_detect_bugs(ssh, s->vstring); - - /* - * Decide which SSH protocol version to support. - */ - s->version = dupprintf( - "%.*s", (int)strcspn(s->vstring + strlen(protoname), "-"), - s->vstring + strlen(protoname)); - - /* Anything strictly below "2.0" means protocol 1 is supported. */ - s->proto1 = ssh_versioncmp(s->version, "2.0") < 0; - /* Anything greater or equal to "1.99" means protocol 2 is supported. */ - s->proto2 = ssh_versioncmp(s->version, "1.99") >= 0; - - if (conf_get_int(ssh->conf, CONF_sshprot) == 0) { - if (!s->proto1) { - bombout(("SSH protocol version 1 required by our configuration " - "but not provided by server")); - crStopV; - } - } else if (conf_get_int(ssh->conf, CONF_sshprot) == 3) { - if (!s->proto2) { - bombout(("SSH protocol version 2 required by our configuration " - "but server only provides (old, insecure) SSH-1")); - crStopV; - } } else { - /* No longer support values 1 or 2 for CONF_sshprot */ - assert(!"Unexpected value for CONF_sshprot"); + assert(ssh->version == 2); /* can't do SSH-1 bare connection! */ + logeventf(ssh, "Using bare ssh-connection protocol"); + + ssh2_bare_connection_protocol_setup(ssh); + ssh->current_user_input_fn = ssh2_connection_input; + } - if (s->proto2 && (conf_get_int(ssh->conf, CONF_sshprot) >= 2 || !s->proto1)) - ssh->version = 2; - else - ssh->version = 1; - - logeventf(ssh, "Using SSH protocol version %d", ssh->version); - - /* Send the version string, if we haven't already */ - if (conf_get_int(ssh->conf, CONF_sshprot) != 3) - ssh_send_verstring(ssh, protoname, s->version); - - sfree(s->version); - - if (ssh->version == 2) { - size_t len; - /* - * Record their version string. - */ - len = strcspn(s->vstring, "\015\012"); - ssh->v_s = snewn(len + 1, char); - memcpy(ssh->v_s, s->vstring, len); - ssh->v_s[len] = 0; - - /* - * Initialise SSH-2 protocol. - */ - ssh2_protocol_setup(ssh); - ssh->general_packet_processing = ssh2_general_packet_processing; - ssh->current_user_input_fn = NULL; - } else { - /* - * Initialise SSH-1 protocol. - */ - ssh1_protocol_setup(ssh); - ssh->current_user_input_fn = ssh1_login_input; - } ssh->bpp->out_raw = &ssh->outgoing_data; ssh->bpp->in_raw = &ssh->incoming_data; ssh->bpp->in_pq = &ssh->pq_full; ssh->bpp->pls = &ssh->pls; ssh->bpp->logctx = ssh->logctx; - ssh->current_incoming_data_fn = ssh_feed_to_bpp; queue_idempotent_callback(&ssh->incoming_data_consumer); queue_idempotent_callback(&ssh->user_input_consumer); - if (ssh->version == 2) - queue_idempotent_callback(&ssh->ssh2_transport_icb); - - update_specials_menu(ssh->frontend); - ssh->state = SSH_STATE_BEFORE_SIZE; - ssh->pinger = pinger_new(ssh->conf, &ssh->backend); - - sfree(s->vstring); - - crFinishV; -} - -static void do_ssh_connection_init(Ssh ssh) -{ - /* - * Ordinary SSH begins with the banner "SSH-x.y-...". This is just - * the ssh-connection part, extracted and given a trivial binary - * packet protocol, so we replace 'SSH-' at the start with a new - * name. In proper SSH style (though of course this part of the - * proper SSH protocol _isn't_ subject to this kind of - * DNS-domain-based extension), we define the new name in our - * extension space. - */ - static const char protoname[] = - "SSHCONNECTION@putty.projects.tartarus.org-"; - - struct do_ssh_connection_init_state { - int crLine; - int vslen; - char *vstring; - char *version; - int vstrsize; - int i; - }; - crState(do_ssh_connection_init_state); - - crBeginState; - - /* - * Search for a line beginning with the protocol name prefix in - * the input. - */ - s->i = 0; - while (1) { - char prefix[sizeof(protoname)-1]; - - /* - * Every time round this loop, we're at the start of a new - * line, so look for the prefix. - */ - crMaybeWaitUntilV( - bufchain_size(&ssh->incoming_data) >= sizeof(prefix)); - bufchain_fetch(&ssh->incoming_data, prefix, sizeof(prefix)); - if (!memcmp(prefix, protoname, sizeof(prefix))) { - bufchain_consume(&ssh->incoming_data, sizeof(prefix)); - break; - } - - /* - * If we didn't find it, consume data until we see a newline. - */ - while (1) { - int len; - void *data; - char *nl; - - crMaybeWaitUntilV(bufchain_size(&ssh->incoming_data) > 0); - bufchain_prefix(&ssh->incoming_data, &data, &len); - if ((nl = memchr(data, '\012', len)) != NULL) { - bufchain_consume(&ssh->incoming_data, nl - (char *)data + 1); - break; - } else { - bufchain_consume(&ssh->incoming_data, len); - } - } - } - - /* - * Now read the rest of the greeting line. - */ - s->vstrsize = sizeof(protoname) + 16; - s->vstring = snewn(s->vstrsize, char); - strcpy(s->vstring, protoname); - s->vslen = strlen(protoname); - s->i = 0; - do { - int len; - void *data; - char *nl; - - crMaybeWaitUntilV(bufchain_size(&ssh->incoming_data) > 0); - bufchain_prefix(&ssh->incoming_data, &data, &len); - if ((nl = memchr(data, '\012', len)) != NULL) { - len = nl - (char *)data + 1; - } - - if (s->vslen + len >= s->vstrsize - 1) { - s->vstrsize = (s->vslen + len) * 5 / 4 + 32; - s->vstring = sresize(s->vstring, s->vstrsize, char); - } - - memcpy(s->vstring + s->vslen, data, len); - s->vslen += len; - bufchain_consume(&ssh->incoming_data, len); - - } while (s->vstring[s->vslen-1] != '\012'); - - s->vstring[s->vslen] = 0; - s->vstring[strcspn(s->vstring, "\015\012")] = '\0';/* remove EOL chars */ - - ssh->agentfwd_enabled = FALSE; - - logeventf(ssh, "Server version: %s", s->vstring); - ssh_detect_bugs(ssh, s->vstring); - - /* - * Decide which SSH protocol version to support. This is easy in - * bare ssh-connection mode: only 2.0 is legal. - */ - s->version = dupprintf( - "%.*s", (int)strcspn(s->vstring + strlen(protoname), "-"), - s->vstring + strlen(protoname)); - - if (ssh_versioncmp(s->version, "2.0") < 0) { - bombout(("Server announces compatibility with SSH-1 in bare ssh-connection protocol")); - crStopV; - } - if (conf_get_int(ssh->conf, CONF_sshprot) == 0) { - bombout(("Bare ssh-connection protocol cannot be run in SSH-1-only mode")); - crStopV; - } - - ssh->version = 2; - - logeventf(ssh, "Using bare ssh-connection protocol"); - - /* Send the version string, if we haven't already */ - ssh_send_verstring(ssh, protoname, s->version); - - sfree(s->version); - - /* - * Initialise bare connection protocol. - */ - ssh2_bare_connection_protocol_setup(ssh); - ssh->bpp->out_raw = &ssh->outgoing_data; - ssh->bpp->in_raw = &ssh->incoming_data; - ssh->bpp->in_pq = &ssh->pq_full; - ssh->bpp->pls = &ssh->pls; - ssh->bpp->logctx = ssh->logctx; - ssh->current_incoming_data_fn = ssh_feed_to_bpp; - queue_idempotent_callback(&ssh->incoming_data_consumer); - ssh->current_user_input_fn = ssh2_connection_input; - queue_idempotent_callback(&ssh->user_input_consumer); update_specials_menu(ssh->frontend); ssh->state = SSH_STATE_BEFORE_SIZE; ssh->pinger = pinger_new(ssh->conf, &ssh->backend); - /* - * Get connection protocol under way. - */ - do_ssh2_connection(ssh); - - sfree(s->vstring); - - crFinishV; + if (ssh->bare_connection) { + /* + * Get connection protocol under way. + */ + do_ssh2_connection(ssh); + } } static void ssh_set_frozen(Ssh ssh, int frozen) @@ -1981,7 +1450,7 @@ static void ssh_process_incoming_data(void *ctx) return; if (!ssh->frozen) - ssh->current_incoming_data_fn(ssh); + ssh_feed_to_bpp(ssh); if (ssh->state == SSH_STATE_CLOSED) /* yes, check _again_ */ return; @@ -2317,7 +1786,6 @@ static const char *connect_to_host(Ssh ssh, const char *host, int port, * We are a downstream. */ ssh->bare_connection = TRUE; - ssh->current_incoming_data_fn = do_ssh_connection_init; ssh->fullhostname = NULL; *realhost = dupstr(host); /* best we can do */ @@ -2332,7 +1800,6 @@ static const char *connect_to_host(Ssh ssh, const char *host, int port, /* * We're not a downstream, so open a normal socket. */ - ssh->current_incoming_data_fn = do_ssh_init; /* * Try to find host. @@ -2358,20 +1825,35 @@ static const char *connect_to_host(Ssh ssh, const char *host, int port, /* * The SSH version number is always fixed (since we no longer support - * fallback between versions), so set it now, and if it's SSH-2, - * send the version string now too. + * fallback between versions), so set it now. */ sshprot = conf_get_int(ssh->conf, CONF_sshprot); assert(sshprot == 0 || sshprot == 3); if (sshprot == 0) /* SSH-1 only */ ssh->version = 1; - if (sshprot == 3 && !ssh->bare_connection) { + if (sshprot == 3 || ssh->bare_connection) { /* SSH-2 only */ ssh->version = 2; - ssh_send_verstring(ssh, "SSH-", NULL); } + /* + * Set up the initial BPP that will do the version string + * exchange. + */ + ssh->version_receiver.got_ssh_version = ssh_got_ssh_version; + ssh->bpp = ssh_verstring_new( + ssh->conf, ssh->frontend, ssh->bare_connection, + ssh->version == 1 ? "1.5" : "2.0", &ssh->version_receiver); + ssh->bpp->out_raw = &ssh->outgoing_data; + ssh->bpp->in_raw = &ssh->incoming_data; + /* + * And call its handle_input method right now, in case it wants to + * send the outgoing version string immediately. + */ + ssh_bpp_handle_input(ssh->bpp); + queue_idempotent_callback(&ssh->outgoing_data_sender); + /* * loghost, if configured, overrides realhost. */ diff --git a/sshbpp.h b/sshbpp.h index 6e45262e..a8820cf1 100644 --- a/sshbpp.h +++ b/sshbpp.h @@ -50,4 +50,21 @@ void ssh2_bpp_new_incoming_crypto( BinaryPacketProtocol *ssh2_bare_bpp_new(void); +/* + * The initial code to handle the SSH version exchange is also + * structured as an implementation of BinaryPacketProtocol, because + * that makes it easy to switch from that to the next BPP once it + * tells us which one we're using. + */ +struct ssh_version_receiver { + void (*got_ssh_version)(struct ssh_version_receiver *rcv, + int major_version); +}; +BinaryPacketProtocol *ssh_verstring_new( + Conf *conf, Frontend *frontend, int bare_connection_mode, + const char *protoversion, struct ssh_version_receiver *rcv); +const char *ssh_verstring_get_remote(BinaryPacketProtocol *); +const char *ssh_verstring_get_local(BinaryPacketProtocol *); +int ssh_verstring_get_bugs(BinaryPacketProtocol *); + #endif /* PUTTY_SSHBPP_H */ diff --git a/sshverstring.c b/sshverstring.c new file mode 100644 index 00000000..a9b1e46c --- /dev/null +++ b/sshverstring.c @@ -0,0 +1,602 @@ +/* + * Code to handle the initial SSH version string exchange. + */ + +#include +#include +#include + +#include "putty.h" +#include "ssh.h" +#include "sshbpp.h" +#include "sshcr.h" + +#define PREFIX_MAXLEN 64 + +struct ssh_verstring_state { + int crState; + + Conf *conf; + Frontend *frontend; + ptrlen prefix_wanted; + char *our_protoversion; + struct ssh_version_receiver *receiver; + + int send_early; + + int found_prefix; + int major_protoversion; + int remote_bugs; + char prefix[PREFIX_MAXLEN]; + char *vstring; + int vslen, vstrsize; + char *protoversion; + const char *softwareversion; + + char *our_vstring; + int i; + + BinaryPacketProtocol bpp; +}; + +static void ssh_verstring_free(BinaryPacketProtocol *bpp); +static void ssh_verstring_handle_input(BinaryPacketProtocol *bpp); +static PktOut *ssh_verstring_new_pktout(int type); +static void ssh_verstring_format_packet(BinaryPacketProtocol *bpp, PktOut *); + +const struct BinaryPacketProtocolVtable ssh_verstring_vtable = { + ssh_verstring_free, + ssh_verstring_handle_input, + ssh_verstring_new_pktout, + ssh_verstring_format_packet, +}; + +static void ssh_detect_bugs(struct ssh_verstring_state *s); +static int ssh_version_includes_v1(const char *ver); +static int ssh_version_includes_v2(const char *ver); + +BinaryPacketProtocol *ssh_verstring_new( + Conf *conf, Frontend *frontend, int bare_connection_mode, + const char *protoversion, struct ssh_version_receiver *rcv) +{ + struct ssh_verstring_state *s = snew(struct ssh_verstring_state); + + memset(s, 0, sizeof(struct ssh_verstring_state)); + + if (!bare_connection_mode) { + s->prefix_wanted = PTRLEN_LITERAL("SSH-"); + } else { + /* + * Ordinary SSH begins with the banner "SSH-x.y-...". Here, + * we're going to be speaking just the ssh-connection + * subprotocol, extracted and given a trivial binary packet + * protocol, so we need a new banner. + * + * The new banner is like the ordinary SSH banner, but + * replaces the prefix 'SSH-' at the start with a new name. In + * proper SSH style (though of course this part of the proper + * SSH protocol _isn't_ subject to this kind of + * DNS-domain-based extension), we define the new name in our + * extension space. + */ + s->prefix_wanted = PTRLEN_LITERAL( + "SSHCONNECTION@putty.projects.tartarus.org-"); + } + assert(s->prefix_wanted.len <= PREFIX_MAXLEN); + + s->conf = conf_copy(conf); + s->frontend = frontend; + s->our_protoversion = dupstr(protoversion); + s->receiver = rcv; + + /* + * We send our version string early if we can. But if it includes + * SSH-1, we can't, because we have to take the other end into + * account too (see below). + */ + s->send_early = !ssh_version_includes_v1(protoversion); + + s->bpp.vt = &ssh_verstring_vtable; + return &s->bpp; +} + +void ssh_verstring_free(BinaryPacketProtocol *bpp) +{ + struct ssh_verstring_state *s = + FROMFIELD(bpp, struct ssh_verstring_state, bpp); + conf_free(s->conf); + sfree(s->vstring); + sfree(s->protoversion); + sfree(s->our_vstring); + sfree(s->our_protoversion); + sfree(s); +} + +static int ssh_versioncmp(const char *a, const char *b) +{ + char *ae, *be; + unsigned long av, bv; + + av = strtoul(a, &ae, 10); + bv = strtoul(b, &be, 10); + if (av != bv) + return (av < bv ? -1 : +1); + if (*ae == '.') + ae++; + if (*be == '.') + be++; + av = strtoul(ae, &ae, 10); + bv = strtoul(be, &be, 10); + if (av != bv) + return (av < bv ? -1 : +1); + return 0; +} + +static int ssh_version_includes_v1(const char *ver) +{ + return ssh_versioncmp(ver, "2.0") < 0; +} + +static int ssh_version_includes_v2(const char *ver) +{ + return ssh_versioncmp(ver, "1.99") >= 0; +} + +#define vs_logevent(printf_args) \ + logevent_and_free(s->frontend, dupprintf printf_args) + +static void ssh_verstring_send(struct ssh_verstring_state *s) +{ + char *p; + int sv_pos; + + /* + * Construct our outgoing version string. + */ + s->our_vstring = dupprintf( + "%.*s%s-%s", + (int)s->prefix_wanted.len, (const char *)s->prefix_wanted.ptr, + s->our_protoversion, sshver); + sv_pos = s->prefix_wanted.len + strlen(s->our_protoversion) + 1; + + /* Convert minus signs and spaces in the software version string + * into underscores. */ + for (p = s->our_vstring + sv_pos; *p; p++) { + if (*p == '-' || *p == ' ') + *p = '_'; + } + +#ifdef FUZZING + /* + * Replace the first character of the string with an "I" if we're + * compiling this code for fuzzing - i.e. the protocol prefix + * becomes "ISH-" instead of "SSH-". + * + * This is irrelevant to any real client software (the only thing + * reading the output of PuTTY built for fuzzing is the fuzzer, + * which can adapt to whatever it sees anyway). But it's a safety + * precaution making it difficult to accidentally run such a + * version of PuTTY (which would be hugely insecure) against a + * live peer implementation. + * + * (So the replacement prefix "ISH" notionally stands for + * 'Insecure Shell', of course.) + */ + s->our_vstring[0] = 'I'; +#endif + + /* + * Now send that version string, plus trailing \r\n or just \n + * (the latter in SSH-1 mode). + */ + bufchain_add(s->bpp.out_raw, s->our_vstring, strlen(s->our_vstring)); + if (ssh_version_includes_v2(s->our_protoversion)) + bufchain_add(s->bpp.out_raw, "\015", 1); + bufchain_add(s->bpp.out_raw, "\012", 1); + + vs_logevent(("We claim version: %s", s->our_vstring)); +} + +void ssh_verstring_handle_input(BinaryPacketProtocol *bpp) +{ + struct ssh_verstring_state *s = + FROMFIELD(bpp, struct ssh_verstring_state, bpp); + + crBegin(s->crState); + + /* + * If we're sending our version string up front before seeing the + * other side's, then do it now. + */ + if (s->send_early) + ssh_verstring_send(s); + + /* + * Search for a line beginning with the protocol name prefix in + * the input. + */ + s->i = 0; + while (1) { + /* + * Every time round this loop, we're at the start of a new + * line, so look for the prefix. + */ + crMaybeWaitUntilV(bufchain_size(s->bpp.in_raw) >= + s->prefix_wanted.len); + bufchain_fetch(s->bpp.in_raw, s->prefix, s->prefix_wanted.len); + if (!memcmp(s->prefix, s->prefix_wanted.ptr, s->prefix_wanted.len)) { + bufchain_consume(s->bpp.in_raw, s->prefix_wanted.len); + break; + } + + /* + * If we didn't find it, consume data until we see a newline. + */ + while (1) { + int len; + void *data; + char *nl; + + crMaybeWaitUntilV(bufchain_size(s->bpp.in_raw) > 0); + bufchain_prefix(s->bpp.in_raw, &data, &len); + if ((nl = memchr(data, '\012', len)) != NULL) { + bufchain_consume(s->bpp.in_raw, nl - (char *)data + 1); + break; + } else { + bufchain_consume(s->bpp.in_raw, len); + } + } + } + + s->found_prefix = TRUE; + + /* + * Start a buffer to store the full greeting line. + */ + s->vstrsize = s->prefix_wanted.len + 16; + s->vstring = snewn(s->vstrsize, char); + memcpy(s->vstring, s->prefix_wanted.ptr, s->prefix_wanted.len); + s->vslen = s->prefix_wanted.len; + + /* + * Now read the rest of the greeting line. + */ + s->i = 0; + do { + int len; + void *data; + char *nl; + + crMaybeWaitUntilV(bufchain_size(s->bpp.in_raw) > 0); + bufchain_prefix(s->bpp.in_raw, &data, &len); + if ((nl = memchr(data, '\012', len)) != NULL) { + len = nl - (char *)data + 1; + } + + if (s->vslen + len >= s->vstrsize - 1) { + s->vstrsize = (s->vslen + len) * 5 / 4 + 32; + s->vstring = sresize(s->vstring, s->vstrsize, char); + } + + memcpy(s->vstring + s->vslen, data, len); + s->vslen += len; + bufchain_consume(s->bpp.in_raw, len); + + } while (s->vstring[s->vslen-1] != '\012'); + + /* + * Trim \r and \n from the version string, and replace them with + * a NUL terminator. + */ + while (s->vslen > 0 && + (s->vstring[s->vslen-1] == '\r' || + s->vstring[s->vslen-1] == '\n')) + s->vslen--; + s->vstring[s->vslen] = '\0'; + + vs_logevent(("Remote version: %s", s->vstring)); + + /* + * Pick out the protocol version and software version. The former + * goes in a separately allocated string, so that s->vstring + * remains intact for later use in key exchange; the latter is the + * tail of s->vstring, so it doesn't need to be allocated. + */ + { + const char *pv_start = s->vstring + s->prefix_wanted.len; + int pv_len = strcspn(pv_start, "-"); + s->protoversion = dupprintf("%.*s", pv_len, pv_start); + s->softwareversion = pv_start + pv_len; + if (*s->softwareversion) { + assert(*s->softwareversion == '-'); + s->softwareversion++; + } + } + + ssh_detect_bugs(s); + + /* + * Figure out what actual SSH protocol version we're speaking. + */ + if (ssh_version_includes_v2(s->our_protoversion) && + ssh_version_includes_v2(s->protoversion)) { + /* + * We're doing SSH-2. + */ + s->major_protoversion = 2; + } else if (ssh_version_includes_v1(s->our_protoversion) && + ssh_version_includes_v1(s->protoversion)) { + /* + * We're doing SSH-1. + */ + s->major_protoversion = 1; + + /* + * There are multiple minor versions of SSH-1, and the + * protocol does not specify that the minimum of client + * and server versions is used. So we must adjust our + * outgoing protocol version to be no higher than that of + * the other side. + */ + if (!s->send_early && + ssh_versioncmp(s->our_protoversion, s->protoversion) > 0) { + sfree(s->our_protoversion); + s->our_protoversion = dupstr(s->protoversion); + } + } else { + /* + * Unable to agree on a major protocol version at all. + */ + if (!ssh_version_includes_v2(s->our_protoversion)) { + s->bpp.error = dupstr( + "SSH protocol version 1 required by our configuration " + "but not provided by remote"); + } else { + s->bpp.error = dupstr( + "SSH protocol version 2 required by our configuration " + "but remote only provides (old, insecure) SSH-1"); + } + crStopV; + } + + vs_logevent(("Using SSH protocol version %d", s->major_protoversion)); + + if (!s->send_early) { + /* + * If we didn't send our version string early, construct and + * send it now, because now we know what it is. + */ + ssh_verstring_send(s); + } + + /* + * And we're done. Notify our receiver that we now know our + * protocol version. This will cause it to disconnect us from the + * input stream and ultimately free us, because our job is now + * done. + */ + s->receiver->got_ssh_version(s->receiver, s->major_protoversion); + + crFinishV; +} + +static PktOut *ssh_verstring_new_pktout(int type) +{ + assert(0 && "Should never try to send packets during SSH version " + "string exchange"); + return NULL; +} + +static void ssh_verstring_format_packet(BinaryPacketProtocol *bpp, PktOut *pkg) +{ + assert(0 && "Should never try to send packets during SSH version " + "string exchange"); +} + +/* + * Examine the remote side's version string, and compare it against a + * list of known buggy implementations. + */ +static void ssh_detect_bugs(struct ssh_verstring_state *s) +{ + const char *imp = s->softwareversion; + + s->remote_bugs = 0; + + /* + * General notes on server version strings: + * - Not all servers reporting "Cisco-1.25" have all the bugs listed + * here -- in particular, we've heard of one that's perfectly happy + * with SSH1_MSG_IGNOREs -- but this string never seems to change, + * so we can't distinguish them. + */ + if (conf_get_int(s->conf, CONF_sshbug_ignore1) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_ignore1) == AUTO && + (!strcmp(imp, "1.2.18") || !strcmp(imp, "1.2.19") || + !strcmp(imp, "1.2.20") || !strcmp(imp, "1.2.21") || + !strcmp(imp, "1.2.22") || !strcmp(imp, "Cisco-1.25") || + !strcmp(imp, "OSU_1.4alpha3") || !strcmp(imp, "OSU_1.5alpha4")))) { + /* + * These versions don't support SSH1_MSG_IGNORE, so we have + * to use a different defence against password length + * sniffing. + */ + s->remote_bugs |= BUG_CHOKES_ON_SSH1_IGNORE; + vs_logevent(("We believe remote version has SSH-1 ignore bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_plainpw1) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_plainpw1) == AUTO && + (!strcmp(imp, "Cisco-1.25") || !strcmp(imp, "OSU_1.4alpha3")))) { + /* + * These versions need a plain password sent; they can't + * handle having a null and a random length of data after + * the password. + */ + s->remote_bugs |= BUG_NEEDS_SSH1_PLAIN_PASSWORD; + vs_logevent(("We believe remote version needs a " + "plain SSH-1 password")); + } + + if (conf_get_int(s->conf, CONF_sshbug_rsa1) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_rsa1) == AUTO && + (!strcmp(imp, "Cisco-1.25")))) { + /* + * These versions apparently have no clue whatever about + * RSA authentication and will panic and die if they see + * an AUTH_RSA message. + */ + s->remote_bugs |= BUG_CHOKES_ON_RSA; + vs_logevent(("We believe remote version can't handle SSH-1 " + "RSA authentication")); + } + + if (conf_get_int(s->conf, CONF_sshbug_hmac2) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_hmac2) == AUTO && + !wc_match("* VShell", imp) && + (wc_match("2.1.0*", imp) || wc_match("2.0.*", imp) || + wc_match("2.2.0*", imp) || wc_match("2.3.0*", imp) || + wc_match("2.1 *", imp)))) { + /* + * These versions have the HMAC bug. + */ + s->remote_bugs |= BUG_SSH2_HMAC; + vs_logevent(("We believe remote version has SSH-2 HMAC bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_derivekey2) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_derivekey2) == AUTO && + !wc_match("* VShell", imp) && + (wc_match("2.0.0*", imp) || wc_match("2.0.10*", imp) ))) { + /* + * These versions have the key-derivation bug (failing to + * include the literal shared secret in the hashes that + * generate the keys). + */ + s->remote_bugs |= BUG_SSH2_DERIVEKEY; + vs_logevent(("We believe remote version has SSH-2 " + "key-derivation bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_rsapad2) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_rsapad2) == AUTO && + (wc_match("OpenSSH_2.[5-9]*", imp) || + wc_match("OpenSSH_3.[0-2]*", imp) || + wc_match("mod_sftp/0.[0-8]*", imp) || + wc_match("mod_sftp/0.9.[0-8]", imp)))) { + /* + * These versions have the SSH-2 RSA padding bug. + */ + s->remote_bugs |= BUG_SSH2_RSA_PADDING; + vs_logevent(("We believe remote version has SSH-2 RSA padding bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_pksessid2) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_pksessid2) == AUTO && + wc_match("OpenSSH_2.[0-2]*", imp))) { + /* + * These versions have the SSH-2 session-ID bug in + * public-key authentication. + */ + s->remote_bugs |= BUG_SSH2_PK_SESSIONID; + vs_logevent(("We believe remote version has SSH-2 " + "public-key-session-ID bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_rekey2) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_rekey2) == AUTO && + (wc_match("DigiSSH_2.0", imp) || + wc_match("OpenSSH_2.[0-4]*", imp) || + wc_match("OpenSSH_2.5.[0-3]*", imp) || + wc_match("Sun_SSH_1.0", imp) || + wc_match("Sun_SSH_1.0.1", imp) || + /* All versions <= 1.2.6 (they changed their format in 1.2.7) */ + wc_match("WeOnlyDo-*", imp)))) { + /* + * These versions have the SSH-2 rekey bug. + */ + s->remote_bugs |= BUG_SSH2_REKEY; + vs_logevent(("We believe remote version has SSH-2 rekey bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_maxpkt2) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_maxpkt2) == AUTO && + (wc_match("1.36_sshlib GlobalSCAPE", imp) || + wc_match("1.36 sshlib: GlobalScape", imp)))) { + /* + * This version ignores our makpkt and needs to be throttled. + */ + s->remote_bugs |= BUG_SSH2_MAXPKT; + vs_logevent(("We believe remote version ignores SSH-2 " + "maximum packet size")); + } + + if (conf_get_int(s->conf, CONF_sshbug_ignore2) == FORCE_ON) { + /* + * Servers that don't support SSH2_MSG_IGNORE. Currently, + * none detected automatically. + */ + s->remote_bugs |= BUG_CHOKES_ON_SSH2_IGNORE; + vs_logevent(("We believe remote version has SSH-2 ignore bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_oldgex2) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_oldgex2) == AUTO && + (wc_match("OpenSSH_2.[235]*", imp)))) { + /* + * These versions only support the original (pre-RFC4419) + * SSH-2 GEX request, and disconnect with a protocol error if + * we use the newer version. + */ + s->remote_bugs |= BUG_SSH2_OLDGEX; + vs_logevent(("We believe remote version has outdated SSH-2 GEX")); + } + + if (conf_get_int(s->conf, CONF_sshbug_winadj) == FORCE_ON) { + /* + * Servers that don't support our winadj request for one + * reason or another. Currently, none detected automatically. + */ + s->remote_bugs |= BUG_CHOKES_ON_WINADJ; + vs_logevent(("We believe remote version has winadj bug")); + } + + if (conf_get_int(s->conf, CONF_sshbug_chanreq) == FORCE_ON || + (conf_get_int(s->conf, CONF_sshbug_chanreq) == AUTO && + (wc_match("OpenSSH_[2-5].*", imp) || + wc_match("OpenSSH_6.[0-6]*", imp) || + wc_match("dropbear_0.[2-4][0-9]*", imp) || + wc_match("dropbear_0.5[01]*", imp)))) { + /* + * These versions have the SSH-2 channel request bug. + * OpenSSH 6.7 and above do not: + * https://bugzilla.mindrot.org/show_bug.cgi?id=1818 + * dropbear_0.52 and above do not: + * https://secure.ucc.asn.au/hg/dropbear/rev/cd02449b709c + */ + s->remote_bugs |= BUG_SENDS_LATE_REQUEST_REPLY; + vs_logevent(("We believe remote version has SSH-2 " + "channel request bug")); + } +} + +const char *ssh_verstring_get_remote(BinaryPacketProtocol *bpp) +{ + struct ssh_verstring_state *s = + FROMFIELD(bpp, struct ssh_verstring_state, bpp); + return s->vstring; +} + +const char *ssh_verstring_get_local(BinaryPacketProtocol *bpp) +{ + struct ssh_verstring_state *s = + FROMFIELD(bpp, struct ssh_verstring_state, bpp); + return s->our_vstring; +} + +int ssh_verstring_get_bugs(BinaryPacketProtocol *bpp) +{ + struct ssh_verstring_state *s = + FROMFIELD(bpp, struct ssh_verstring_state, bpp); + return s->remote_bugs; +}