Skip to content

Commit beb2a97

Browse files
committed
ssh/knownhosts: disregard IP address if the hostname is available
This fixes the following vulnerability scenario: * Victim logs into SAFE-HOST on SAFE-IP-ADDRESS regularly. * Victim is cajoled into connecting to attacker controlled ATTACK-HOST, on ATTACK-IP-ADDRESS. ATTACK-HOST uses a different host key type (e.g. Ed25519 vs RSA). The new key is added at the end of known_hosts. * Attacker makes DNS system return ATTACK-IP-ADDRESS for SAFE-HOST. * Victim logs into SAFE-HOST, but is not warned because the host key matches ATTACK-IP-ADDRESS. For this attack to work, the key type has to be different, because knownhosts gives precedence to the first key found for each type. Add a test that asserts this behavior. The new semantics simplify the code, but callers that modify .ssh/known_host interactviely must now take an extra step to remain OpenSSH compatible: on successful login, the IP address must be checked without hostname, and if it is not known, added separately to the known_hosts file, so future logins that use an IP address only will be protected too. Thanks to Daniel Parks <[email protected]> for finding this vulnerability. Change-Id: I62b1b60ceb02e2f583a4657213feac1a8885dd42 Reviewed-on: https://go-review.googlesource.com/104939 Reviewed-by: Adam Langley <[email protected]> Run-TryBot: Han-Wen Nienhuys <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent b2aa354 commit beb2a97

File tree

2 files changed

+54
-33
lines changed

2 files changed

+54
-33
lines changed

ssh/knownhosts/knownhosts.go

+26-32
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
// Package knownhosts implements a parser for the OpenSSH
6-
// known_hosts host key database.
5+
// Package knownhosts implements a parser for the OpenSSH known_hosts
6+
// host key database, and provides utility functions for writing
7+
// OpenSSH compliant known_hosts files.
78
package knownhosts
89

910
import (
@@ -38,7 +39,7 @@ func (a *addr) String() string {
3839
}
3940

4041
type matcher interface {
41-
match([]addr) bool
42+
match(addr) bool
4243
}
4344

4445
type hostPattern struct {
@@ -57,19 +58,16 @@ func (p *hostPattern) String() string {
5758

5859
type hostPatterns []hostPattern
5960

60-
func (ps hostPatterns) match(addrs []addr) bool {
61+
func (ps hostPatterns) match(a addr) bool {
6162
matched := false
6263
for _, p := range ps {
63-
for _, a := range addrs {
64-
m := p.match(a)
65-
if !m {
66-
continue
67-
}
68-
if p.negate {
69-
return false
70-
}
71-
matched = true
64+
if !p.match(a) {
65+
continue
7266
}
67+
if p.negate {
68+
return false
69+
}
70+
matched = true
7371
}
7472
return matched
7573
}
@@ -122,8 +120,8 @@ func serialize(k ssh.PublicKey) string {
122120
return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal())
123121
}
124122

125-
func (l *keyDBLine) match(addrs []addr) bool {
126-
return l.matcher.match(addrs)
123+
func (l *keyDBLine) match(a addr) bool {
124+
return l.matcher.match(a)
127125
}
128126

129127
type hostKeyDB struct {
@@ -153,7 +151,7 @@ func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool
153151
a := addr{host: h, port: p}
154152

155153
for _, l := range db.lines {
156-
if l.cert && keyEq(l.knownKey.Key, remote) && l.match([]addr{a}) {
154+
if l.cert && keyEq(l.knownKey.Key, remote) && l.match(a) {
157155
return true
158156
}
159157
}
@@ -338,34 +336,32 @@ func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.Public
338336
return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", remote, err)
339337
}
340338

341-
addrs := []addr{
342-
{host, port},
343-
}
344-
339+
hostToCheck := addr{host, port}
345340
if address != "" {
341+
// Give preference to the hostname if available.
346342
host, port, err := net.SplitHostPort(address)
347343
if err != nil {
348344
return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", address, err)
349345
}
350346

351-
addrs = append(addrs, addr{host, port})
347+
hostToCheck = addr{host, port}
352348
}
353349

354-
return db.checkAddrs(addrs, remoteKey)
350+
return db.checkAddr(hostToCheck, remoteKey)
355351
}
356352

357353
// checkAddrs checks if we can find the given public key for any of
358354
// the given addresses. If we only find an entry for the IP address,
359355
// or only the hostname, then this still succeeds.
360-
func (db *hostKeyDB) checkAddrs(addrs []addr, remoteKey ssh.PublicKey) error {
356+
func (db *hostKeyDB) checkAddr(a addr, remoteKey ssh.PublicKey) error {
361357
// TODO(hanwen): are these the right semantics? What if there
362358
// is just a key for the IP address, but not for the
363359
// hostname?
364360

365361
// Algorithm => key.
366362
knownKeys := map[string]KnownKey{}
367363
for _, l := range db.lines {
368-
if l.match(addrs) {
364+
if l.match(a) {
369365
typ := l.knownKey.Key.Type()
370366
if _, ok := knownKeys[typ]; !ok {
371367
knownKeys[typ] = l.knownKey
@@ -414,7 +410,10 @@ func (db *hostKeyDB) Read(r io.Reader, filename string) error {
414410

415411
// New creates a host key callback from the given OpenSSH host key
416412
// files. The returned callback is for use in
417-
// ssh.ClientConfig.HostKeyCallback.
413+
// ssh.ClientConfig.HostKeyCallback. By preference, the key check
414+
// operates on the hostname if available, i.e. if a server changes its
415+
// IP address, the host key check will still succeed, even though a
416+
// record of the new IP address is not available.
418417
func New(files ...string) (ssh.HostKeyCallback, error) {
419418
db := newHostKeyDB()
420419
for _, fn := range files {
@@ -536,11 +535,6 @@ func newHashedHost(encoded string) (*hashedHost, error) {
536535
return &hashedHost{salt: salt, hash: hash}, nil
537536
}
538537

539-
func (h *hashedHost) match(addrs []addr) bool {
540-
for _, a := range addrs {
541-
if bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash) {
542-
return true
543-
}
544-
}
545-
return false
538+
func (h *hashedHost) match(a addr) bool {
539+
return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
546540
}

ssh/knownhosts/knownhosts_test.go

+28-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func TestBasic(t *testing.T) {
166166
str := fmt.Sprintf("#comment\n\nserver.org,%s %s\notherhost %s", testAddr, edKeyStr, ecKeyStr)
167167
db := testDB(t, str)
168168
if err := db.check("server.org:22", testAddr, edKey); err != nil {
169-
t.Errorf("got error %q, want none", err)
169+
t.Errorf("got error %v, want none", err)
170170
}
171171

172172
want := KnownKey{
@@ -185,6 +185,33 @@ func TestBasic(t *testing.T) {
185185
}
186186
}
187187

188+
func TestHostNamePrecedence(t *testing.T) {
189+
var evilAddr = &net.TCPAddr{
190+
IP: net.IP{66, 66, 66, 66},
191+
Port: 22,
192+
}
193+
194+
str := fmt.Sprintf("server.org,%s %s\nevil.org,%s %s", testAddr, edKeyStr, evilAddr, ecKeyStr)
195+
db := testDB(t, str)
196+
197+
if err := db.check("server.org:22", evilAddr, ecKey); err == nil {
198+
t.Errorf("check succeeded")
199+
} else if _, ok := err.(*KeyError); !ok {
200+
t.Errorf("got %T, want *KeyError", err)
201+
}
202+
}
203+
204+
func TestDBOrderingPrecedenceKeyType(t *testing.T) {
205+
str := fmt.Sprintf("server.org,%s %s\nserver.org,%s %s", testAddr, edKeyStr, testAddr, alternateEdKeyStr)
206+
db := testDB(t, str)
207+
208+
if err := db.check("server.org:22", testAddr, alternateEdKey); err == nil {
209+
t.Errorf("check succeeded")
210+
} else if _, ok := err.(*KeyError); !ok {
211+
t.Errorf("got %T, want *KeyError", err)
212+
}
213+
}
214+
188215
func TestNegate(t *testing.T) {
189216
str := fmt.Sprintf("%s,!server.org %s", testAddr, edKeyStr)
190217
db := testDB(t, str)

0 commit comments

Comments
 (0)