Next in line is creating SSH clients. The /x/crypto/ssh 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.
First program is a typical interactive session based on the example in the docs. We login with a user/pass combo.
// 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):
// 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.
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.
This is an easy check. We pass a host key and the method checks if it matches the one returned by the connection.
// 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.
// 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.
// 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),
}
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:
type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
In other words, it's a function of this type:
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:
// 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
:
// 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}
...
It's also possible to pass another AuthMethod
and login with a key. Luckily, the package has another example. We read the PEM encoded private key and use it in ClientConfig
.
// 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(),
}
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 must be created for each new command (one SSH connection can support multiple sessions). We can run commands using one of these methods:
- Start: Runs a single command on the server.
- Run: Same as above. In fact, run calls start internally.
- Output: Runs the command but returns standard output.
- CombinedOutput: Runs the command and returns both stdout and stderr.
- Not all of these methods return the output directly (in
[]byte
). For those that do not, we need to readsession.Stdout/Stderr
. - All of them return errors. After execution, check the errors.
- For obvious reasons, it seems like
CombinedOutput
will work best.
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.
// 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
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:
// 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())