Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
samiponkanen opened this issue Jun 6, 2024 · 3 comments
Labels
NeedsFix The path to resolution is known, but the work has not been done. OS-Windows
Milestone

Comments

@samiponkanen
Copy link

samiponkanen commented Jun 6, 2024

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
Copy link
Contributor

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

@seankhliao seankhliao changed the title ssh: wrapping ssh.KeyboardInteractive() into ssh.RetryableAuthMethod() fails to handle early auth errors with Windows OpenSSH server x/crypto/ssh: wrapping ssh.KeyboardInteractive() into ssh.RetryableAuthMethod() fails to handle early auth errors with Windows OpenSSH server Jun 6, 2024
@gopherbot gopherbot added this to the Unreleased milestone Jun 6, 2024
@mknyszek mknyszek added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Jun 7, 2024
@mknyszek
Copy link
Contributor

mknyszek commented Jun 7, 2024

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

@dmitshur dmitshur added NeedsFix The path to resolution is known, but the work has not been done. OS-Windows and removed NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Oct 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsFix The path to resolution is known, but the work has not been done. OS-Windows
Projects
None yet
5 participants