# 04.4 - SSH clients Next in line is creating SSH clients. The [/x/crypto/ssh][ssh-pkg] provides SSH support. It's not one of the standard libraries so you need to `go get golang.org/x/crypto/ssh` before use. We can authenticate using either user/pass or certificate. <!-- MarkdownTOC --> - [Basic interactive session with user/pass](#basic-interactive-session-with-userpass) - [Verifying host](#verifying-host) - [ssh.FixedHostKey](#sshfixedhostkey) - [Custom host verifier](#custom-host-verifier) - [Login with SSH key](#login-with-ssh-key) - [Login and run a command](#login-and-run-a-command) - [Run a command with CombinedOutput](#run-a-command-with-combinedoutput) - [Run a command with Run](#run-a-command-with-run) <!-- /MarkdownTOC --> <a name="basic-interactive-session-with-userpass"></a> ## Basic interactive session with user/pass First program is a typical interactive session based on the [example in the docs][requestpty-example-ssh-pkg]. We login with a user/pass combo. ``` go // 04.4-01-sshclient-login-password.go // Interactive SSH login with user/pass. package main import ( "flag" "fmt" "io" "net" "os" // Importing crypto/ssh "golang.org/x/crypto/ssh" ) var ( username, password, serverIP, serverPort string ) // Read flags func init() { flag.StringVar(&serverPort, "port", "22", "SSH server port") flag.StringVar(&serverIP, "ip", "127.0.0.1", "SSH server IP") flag.StringVar(&username, "user", "", "username") flag.StringVar(&password, "pass", "", "password") } func main() { // Parse flags flag.Parse() // Check if username has been submitted - password can be empty if username == "" { fmt.Println("Must supply username") os.Exit(2) } // Create SSH config config := &ssh.ClientConfig{ // Username User: username, // Each config must have one AuthMethod. In this case we use password Auth: []ssh.AuthMethod{ ssh.Password(password), }, // This callback function validates the server. // Danger! We are ignoring host info HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // Server address t := net.JoinHostPort(serverIP, serverPort) // Connect to the SSH server sshConn, err := ssh.Dial("tcp", t, config) if err != nil { fmt.Printf("Failed to connect to %v\n", t) fmt.Println(err) os.Exit(2) } // Create new SSH session session, err := sshConn.NewSession() if err != nil { fmt.Printf("Cannot create SSH session to %v\n", t) fmt.Println(err) os.Exit(2) } // Close the session when main returns defer session.Close() // For an interactive session we must redirect IO session.Stdout = os.Stdout session.Stderr = os.Stderr input, err := session.StdinPipe() if err != nil { fmt.Println("Error redirecting session input", err) os.Exit(2) } // Setup terminal mode when requesting pty. You can see all terminal modes at // https://github.com/golang/crypto/blob/master/ssh/session.go#L56 or read // the RFC for explanation https://tools.ietf.org/html/rfc4254#section-8 termModes := ssh.TerminalModes{ ssh.ECHO: 0, // Disable echo } // Request pty // https://tools.ietf.org/html/rfc4254#section-6.2 // First variable is term environment variable value which specifies terminal. // term doesn't really matter here, we will use "vt220". // Next are height and width: (40,80) characters and finall termModes. err = session.RequestPty("vt220", 40, 80, termModes) if err != nil { fmt.Println("RequestPty failed", err) os.Exit(2) } // Also // if err = session.RequestPty("vt220", 40, 80, termModes); err != nil { // fmt.Println("RequestPty failed", err) // os.Exit(2) // } // Now we can start a remote shell err = session.Shell() if err != nil { fmt.Println("shell failed", err) os.Exit(2) } // Same as above, a different way to check for errors // if err = session.Shell(); err != nil { // fmt.Println("shell failed", err) // os.Exit(2) // } // Endless loop to capture commands // Note: After exit, we need to ctrl+c to end the application. for { io.Copy(input, os.Stdin) } } ``` First we create a config (note it's a pointer): ``` go // Create SSH config config := &ssh.ClientConfig{ // Username User: username, // Each config must have one AuthMethod. In this case we use password Auth: []ssh.AuthMethod{ ssh.Password(password), }, // This callback function validates the server. // Danger! We are ignoring host info HostKeyCallback: ssh.InsecureIgnoreHostKey(), } ``` Each config should have an `AuthMethod`. We are using a password in this program. Next on the config is `HostKeyCallback` and is used to verify the server. The familiar `Dial` method connects to the server. Then we create a session (each connection can have multiple sessions). We set stdin, stdout and stderr for session and then terminal modes. Finally we request a pseudo-terminal with `RequestPty` and a shell. We capture commands on stdin by basically copying `os.Stdin` to the connection's input. Note: Depending on your SSH server and the terminal mode, you might see color codes. For example you will see ANSI color codes if you run it from Windows cmd, but not in PowerShell. With Windows OpenSSH, it does not matter what TERM is sent, the color codes will not go away in cmd. <a name="verifying-host"></a> ## Verifying host Usually when creating small programs in security, we do not care about the host. But it's always good to check. `HostKeyCallback` in config can be used in three ways: - `ssh.InsecureIgnoreHostKey()`: Ignore everything! - `ssh.FixedHostKey(key PublicKey)`: Returns a function to check the hostkey. - Custom host verifier: Return `nil` if host is ok, otherwise return an error. <a name="sshfixedhostkey"></a> ### ssh.FixedHostKey This is an easy check. We pass a host key and the method checks if it matches the one returned by the connection. ``` go // https://github.com/golang/crypto/blob/master/ssh/client.go#L265 // FixedHostKey returns a function for use in // ClientConfig.HostKeyCallback to accept only a specific host key. func FixedHostKey(key PublicKey) HostKeyCallback { hk := &fixedHostKey{key} return hk.check } ``` Looking at the source, it just unmarshals two publickeys and checks if they match. ``` go // https://github.com/golang/crypto/blob/master/ssh/client.go#L253 func (f *fixedHostKey) check(hostname string, remote net.Addr, key PublicKey) error { if f.key == nil { return fmt.Errorf("ssh: required host key was nil") } if !bytes.Equal(key.Marshal(), f.key.Marshal()) { return fmt.Errorf("ssh: host key mismatch") } return nil } ``` It's straightforward to use. The new program is only a little different from the old one: - Create a variable of type `ssh.PublicKey` to hold the key. - Pass `HostKeyCallback: ssh.FixedHostKey(var_from_above)` in config. ``` go // Define host's public key var hostPubKey ssh.PublicKey // Populate hostPubKey // Create SSH config config := &ssh.ClientConfig{ // Username User: username, // Each config must have one AuthMethod. In this case we use password Auth: []ssh.AuthMethod{ ssh.Password(password), }, // Danger! We are ignoring host info HostKeyCallback: ssh.FixedHostKey(hostPubKey), } ``` <a name="custom-host-verifier"></a> ### Custom host verifier This has more flexibility. We can also use this callback function to grab and store a server's public key. It can have any number of arguments (usually we use these arguments to pass info to the host checker). It should **return a function of type** [ssh.HostKeyCallback][hostkeycallback-ssh-pkg]: ``` go type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error ``` In other words, it's a function of this type: ``` go func hostChecker(arg1 type1, arg2 type2, ...) ssh.HostKeyCallback { // ... } ``` Returned function can be a separate function or an anonymous function created inside `hostChecker`. Here's an example of an anonymous function used by `InsecureIgnoreHostKey` from `ssh` package's source: ``` go // https://github.com/golang/crypto/blob/master/ssh/client.go#L240 // InsecureIgnoreHostKey returns a function that can be used for // ClientConfig.HostKeyCallback to accept any host key. It should // not be used for production code. func InsecureIgnoreHostKey() HostKeyCallback { return func(hostname string, remote net.Addr, key PublicKey) error { return nil } } ``` Now we know enough to create our own custom host checker and pass it to `HostKeyCallback`: ``` go // 04.4-02-sshclient-check-host.go // hostChecker returns a function to be used as callback for HostKeyCallback. func hostChecker() ssh.HostKeyCallback { return printServerKey } // printServerKey prints server's info instead of checking it. // It's of type HostKeyCallback func printServerKey(hostname string, remote net.Addr, key ssh.PublicKey) error { // Just print everything fmt.Printf("Hostname: %v\nRemote address: %v\nServer key: %+v\n", hostname, remote, key) // Return nil so connection can continue without checking the server return nil } ``` We can see server info in the callback function: ``` $ go run 04.4-02-sshclient2.go -user user -pass 12345 Hostname: 127.0.0.1:22 Remote address: 127.0.0.1:22 Server key: &{Curve:{CurveParams:0xc04204e100} X:+95446563830190539723549646387134804373421025763629370453495481728809028570967 Y:+71690030922286766932148563959160819051208718262353076812036347925006921654863} ... ``` <a name="login-with-ssh-key"></a> ## Login with SSH key It's also possible to pass another `AuthMethod` and login with a key. Luckily, the package has [another example][publickey-login-example-ssh-pkg]. We read the PEM encoded private key and use it in `ClientConfig`. ``` go // 04.4-03-sshclient-login-key.go // Now we must read the private key pKey, err := ioutil.ReadFile(pKeyFile) if err != nil { fmt.Println("Failed to read private key from file", err) os.Exit(2) } // Create a signer with the private key signer, err := ssh.ParsePrivateKey(pKey) if err != nil { fmt.Println("Failed to parse private key", err) os.Exit(2) } // Create SSH config config := &ssh.ClientConfig{ // Username User: username, // Each config must have one AuthMethod. Now we use key Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, // This callback function validates the server. // Danger! We are ignoring host info HostKeyCallback: ssh.InsecureIgnoreHostKey(), } ``` <a name="login-and-run-a-command"></a> ## Login and run a command Interactive login is useful but there are SSH clients for that. Automated tools usually want to login, run commands, capture the output and move on to the next host. Each session can only run one command. A [new session][newsession-ssh-pkg] must be created for each new command (one SSH connection can support multiple sessions). We can run commands using one of these methods: - [Start][start-ssh-pkg]: Runs a single command on the server. - [Run][run-ssh-pkg]: Same as above. In fact, [run calls start internally][run-ssh-source]. - [Output][output-ssh-pkg]: Runs the command but returns standard output. - [CombinedOutput][combinedoutput-ssh-pkg]: Runs the command and returns both stdout and stderr. 1. Not all of these methods return the output directly (in `[]byte`). For those that do not, we need to read `session.Stdout/Stderr`. 2. All of them return errors. After execution, check the errors. 3. For obvious reasons, it seems like `CombinedOutput` will work best. <a name="run-a-command-with-combinedoutput"></a> ### Run a command with CombinedOutput We re-use the code from first example but stop after the session is created. Then we run the command, check for errors and print the output. ``` go // 04.4-04-sshclient-run-combinedoutput.go // Close the session when main returns defer session.Close() // Run a command with CombinedOutput o, err := session.CombinedOutput(command) if err != nil { fmt.Println("Error running command", err) } fmt.Printf("Output:\n%s", o) ``` Results from my VM (don't get excited, it's the default user/pass for https://modern.ie VMs): ``` $ go run .\04.4-04-sshclient-run-combinedoutput.go -user IEUser -pass Passw0rd! -cmd dir Output: Volume in drive C is Windows 10 Volume Serial Number is C436-9552 Directory of C:\Users\IEUser 12/19/2017 08:28 PM <DIR> . 12/19/2017 08:28 PM <DIR> .. 10/02/2017 12:50 AM <DIR> .gradle 12/24/2017 07:02 PM <DIR> .ssh 03/23/2017 12:29 PM 6 .vbox_version 03/23/2017 11:18 AM <DIR> Contacts 12/24/2017 01:50 AM <DIR> Desktop ... ``` Of course, we can always cheat by running multiple commands. On Windows use `&` and `&&`. ``` $ go run .\04.4-04-sshclient-run-combinedoutput.go -user IEUser -pass Passw0rd! -cmd "cd .. && dir" Output: Volume in drive C is Windows 10 Volume Serial Number is C436-9552 Directory of C:\Users 12/24/2017 01:53 AM <DIR> . 12/24/2017 01:53 AM <DIR> .. 12/19/2017 08:28 PM <DIR> IEUser 03/23/2017 11:18 AM <DIR> Public 12/24/2017 01:53 AM <DIR> SSHD 0 File(s) 0 bytes 5 Dir(s) 21,863,829,504 bytes free ``` <a name="run-a-command-with-run"></a> ### Run a command with Run Using `Run` is similar, we buffer `session.Stdout/Stderr` before we execute the command and print them after. This is based on the [package example][run-example-ssh-pkg]: ``` go // 04.4-05-sshclient-run-run.go // Close the session when main returns defer session.Close() // Create buffers for stdout and stderr var o, e bytes.Buffer session.Stdout = &o session.Stderr = &e // Run a command with Run and read stdout and stderr if err := session.Run(command); err != nil { fmt.Println("Error running command", err) } // Convert buffer to string fmt.Printf("stdout:\n%s\nstderr:\n%s", o.String(), e.String()) ``` #### Continue reading ⇒ [04.5 - SSH Harvester](04.5.md) <!-- Links --> [ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh [hostkey-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#HostKeyCallback [requestpty-example-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#example-Session-RequestPty [hostkeycallback-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#HostKeyCallback [keyboard-interactive-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#KeyboardInteractive [keyboard-interactive-challenge-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#KeyboardInteractiveChallenge [publickey-login-example-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#example-PublicKeys [run-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#Session.Run [start-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#Session.Start [run-ssh-source]: https://github.com/golang/crypto/blob/master/ssh/session.go#L296 [output-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#Session.Output [combinedoutput-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#Session.CombinedOutput [run-example-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#example-Dial [newsession-ssh-pkg]: https://godoc.org/golang.org/x/crypto/ssh#Client.NewSession