golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.22k stars 17.57k forks source link

x/crypto/ssh: wrapping ssh.KeyboardInteractive() into ssh.RetryableAuthMethod() fails to handle early auth errors with Windows OpenSSH server #67855

Open samiponkanen opened 3 months ago

samiponkanen commented 3 months ago

Go version

go version go1.22.3 linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/xxx/.cache/go-build'
GOENV='/home/xxx/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/xxx/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/xxx/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/lib/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/lib/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.22.3'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/xxx/go/src/github.com/samiponkanen/crypto/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2434574384=/tmp/go-build -gno-record-gcc-switches'

What did you do?

It seems that Windows OpenSSH server behaves incorrectly w.r.t keyboard-interactive authentication:

$ ssh -vvv -o "PubkeyAuthentication no" -o "PasswordAuthentication no" user@10.1.102.148
OpenSSH_9.1p1, OpenSSL 3.0.2 15 Mar 2022
...
debug1: Local version string SSH-2.0-OpenSSH_9.1
debug1: Remote protocol version 2.0, remote software version OpenSSH_for_Windows_8.1
debug1: compat_banner: match: OpenSSH_for_Windows_8.1 pat OpenSSH* compat 0x04000000
...
debug3: receive packet: type 6
debug2: service_accept: ssh-userauth
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug3: send packet: type 50
debug3: receive packet: type 51
debug1: Authentications that can continue: publickey,password,keyboard-interactive
debug3: start over, passed a different list publickey,password,keyboard-interactive
debug3: preferred keyboard-interactive
debug3: authmethod_lookup keyboard-interactive
debug3: remaining preferred: 
debug3: authmethod_is_enabled keyboard-interactive
debug1: Next authentication method: keyboard-interactive
debug2: userauth_kbdint
debug3: send packet: type 50
debug2: we sent a keyboard-interactive packet, wait for reply
debug3: receive packet: type 51
debug1: Authentications that can continue: publickey,password,keyboard-interactive
debug3: userauth_kbdint: disable: no info_req_seen
debug2: we did not send a packet, disable method
debug1: No more authentication methods to try.
user@10.1.102.148: Permission denied (publickey,password,keyboard-interactive).

When trying to connect to such host using a golang client that uses ssh.KeyboardInteractive() wrapped a into ssh.RetryableAuthMethod(), then ssh.RetryableAuthMethod() will retry ssh.KeyboardInteractive() even if the failure happens so early that password is never prompted from the user.

What did you see happen?

package main

import (
    "log"
    "net"
    "os"
    "strings"

    "golang.org/x/crypto/ssh"
)

func main() {
    exit := func(v interface{}) {
        l := log.New(os.Stderr, "", 0)
        l.Printf("%v\n", v)
        os.Exit(-1)
    }

    args := os.Args[1:]
    if len(args) != 1 {
        exit("missing destination")
    }
    idx := strings.LastIndex(args[0], "@")
    if idx == -1 {
        exit("destination does not contain username")
    }
    user := args[0][:idx]
    dst := args[0][idx+1:]
    host, port, err := net.SplitHostPort(dst)
    if err != nil || port == "" {
        host = dst
        port = "22"
    }
    dst = net.JoinHostPort(host, port)

    cfg := &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.RetryableAuthMethod(ssh.KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
                log.Printf("KeyboardInteractive()")
                return []string{"notaverysecretpassword"}, nil
            }), 6),
            ssh.RetryableAuthMethod(ssh.PasswordCallback(func() (secret string, err error) {
                log.Printf("PasswordCallback()")
                return "notaverysecretpassword", nil
            }), 6),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    log.Printf("connecting to %s@%s", user, dst)
    conn, err := ssh.Dial("tcp", dst, cfg)
    if err != nil {
        exit(err)
    }
    conn.Close()
}

Running this test client against a Windows OpenSSH server (and assuming MaxAuthTries is 6) reveals that neither KeyboardInteractive nor PasswordCallback is called:

$ ./testclient user@10.1.102.148
2024/06/06 12:02:05 connecting to user@10.1.102.148:22
ssh: handshake failed: ssh: disconnect, reason 2: Too many authentication failures

What did you expect to see?

Expected result is that PasswordCallback gets called:

$ ./testclient user@10.1.102.148
2024/06/06 12:02:42 connecting to user@10.1.102.148:22
2024/06/06 12:02:42 PasswordCallback()
2024/06/06 12:02:42 PasswordCallback()
2024/06/06 12:02:42 PasswordCallback()
2024/06/06 12:02:43 PasswordCallback()
2024/06/06 12:02:43 PasswordCallback()
ssh: handshake failed: ssh: disconnect, reason 2: Too many authentication failures
gopherbot commented 3 months ago

Change https://go.dev/cl/590956 mentions this issue: ssh: fail keyboard-interactive auth with unexpectedMessageError() when auth fails before receiving the UserAuthInfoRequest from server

mknyszek commented 3 months ago

CC @drakkan @golang/security via https://dev.golang.org/owners

gabyhelp commented 3 months ago

Similar Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)