安全外壳(SSH是一种用于在不安全网络上通信的加密网络协议。SSH 最常见的用途是连接到远程服务器并与 shell 交互。文件传输也通过 SSH 协议通过 SCP 和 SFTP 使用。创建 SSH 是为了取代明文协议 Telnet。随着时间的推移,已经有许多 RFC 来定义 SSH。下面是一个部分列表,让您了解定义的内容。由于它是一个如此常见和关键的协议,因此值得花时间了解细节。以下是一些 RFC:
- RFC 4250https://tools.ietf.org/html/rfc4250 :安全外壳(SSH)协议分配编号
- RFC 4251https://tools.ietf.org/html/rfc4251 :安全外壳(SSH)协议架构
- RFC 4252https://tools.ietf.org/html/rfc4252 :安全 Shell(SSH)认证协议
- RFC 4253 (https://tools.ietf.org/html/rfc4253): The Secure Shell (SSH) Transport Layer Protocol
- RFC 4254https://tools.ietf.org/html/rfc4254 :安全外壳(SSH)连接协议
- RFC 4255https://tools.ietf.org/html/rfc4255 :使用 DNS 安全发布安全外壳(SSH)密钥指纹
- RFC 4256https://tools.ietf.org/html/rfc4256 :安全外壳协议(SSH)的通用消息交换身份验证
- RFC 4335https://tools.ietf.org/html/rfc4335 :安全 Shell(SSH)会话通道中断扩展
- RFC 4344https://tools.ietf.org/html/rfc4344 :安全外壳(SSH)传输层加密模式
- RFC 4345https://tools.ietf.org/html/rfc4345 :针对安全外壳(SSH)传输层协议改进了四种模式
后来,该标准还进行了额外的扩展,您可以在上阅读 https://en.wikipedia.org/wiki/Secure_Shell#Standards_documentation 。
SSH 是 internet 上暴力攻击和默认凭据攻击的常见目标。出于这个原因,您可能会考虑将 SSH 置于非标准端口上,但将其保存到系统端口(小于 1024),以便如果服务下降,低特权用户就不可能劫持端口。如果您将 SSH 保留在默认端口上,那么像fail2ban
这样的服务对于限制速率和阻止暴力攻击来说是非常宝贵的。理想情况下,完全禁用密码身份验证,并且需要密钥身份验证。
SSH 包没有随标准库一起打包,尽管它是由 Go 团队编写的。它是 Go 项目的正式组成部分,但在主 Go 源代码树之外,因此默认情况下不随 Go 一起安装。可从获取 https://golang.org/ 并可使用以下命令进行安装:
go get golang.org/x/crypto/ssh
在本章中,我们将介绍如何使用 SSH 客户端连接、执行命令和使用交互式 shell。我们还将介绍不同的身份验证方法,例如使用密码或私钥。SSH 包提供了创建服务器的功能,但在本书中我们将只介绍客户机。
本章将专门介绍 SSH 的以下内容:
- 使用密码进行身份验证
- 使用私钥进行身份验证
- 验证远程主机的密钥
- 通过 SSH 执行命令
- 启动交互式 shell
golang.org/x/crypto/ssh
包提供与 SSH 版本 2(最新版本)兼容的 SSH 客户端。客户端将与 OpenSSH 服务器和遵循 SSH 规范的任何其他服务器一起工作。它支持传统的客户端功能,如子进程、端口转发和隧道。
身份验证不仅是第一步,也是最关键的一步。不正确的身份验证可能导致机密性、完整性和可用性的潜在损失。如果远程服务器未经验证,可能会发生中间人攻击,从而导致间谍、操纵或数据阻塞。暴力攻击可以利用弱密码身份验证。
这里提供了三个例子。第一个示例涉及密码身份验证,这是常见的,但不推荐使用,因为与加密密钥相比,密码的熵和位计数较低。第二个示例演示如何使用私钥向远程服务器进行身份验证。这两个示例都忽略了远程主机提供的公钥。这是不安全的,因为您可能最终连接到一个您不信任的远程主机,但是对于测试来说已经足够好了。身份验证的第三个示例是理想流。它使用密钥进行身份验证并验证远程服务器。
请注意,本章未使用 PEM 格式的密钥文件,如第 6 章、加密中所述。这使用 SSH 格式的密钥,这自然是使用 SSH 最常见的格式。这些示例与 OpenSSH 工具和密钥兼容,例如ssh
、sshd
、ssh-keygen
、ssh-copy-id
和ssh-keyscan
。
我建议您使用ssh-keygen
生成用于身份验证的公钥和私钥对。这将生成 SSH 密钥格式的id_rsa
和id_rsa.pub
文件。ssh-keygen
工具是 OpenSSH 项目的一部分,默认情况下与 Ubuntu 打包:
ssh-keygen
使用ssh-copy-id
将您的公钥(id_rsa.pub
复制到远程服务器的~/.ssh/authorized_keys
文件,以便您可以使用私钥进行身份验证:
ssh-copy-id yourserver.com
SSH 上的密码身份验证是最简单的方法。此示例演示如何使用ssh.ClientConfig
结构配置 SSH 客户端,然后使用ssh.Dial()
连接到 SSH 服务器。客户端通过指定ssh.Password()
作为身份验证功能配置为使用密码:
package main
import (
"golang.org/x/crypto/ssh"
"log"
)
var username = "username"
var password = "password"
var host = "example.com:22"
func main() {
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", host, config)
if err != nil {
log.Fatal("Error dialing server. ", err)
}
log.Println(string(client.ClientVersion()))
}
私钥比密码有一些优点。它比密码长得多,这使得使用暴力变得更加困难。它还消除了输入密码的需要,从而方便了连接到远程服务器。无密码身份验证也有助于 cron 作业和其他需要在无需人工干预的情况下自动运行的服务。有些服务器完全禁用密码身份验证,并需要密钥。
远程服务器需要将您的公钥作为授权密钥,然后才能使用私钥进行身份验证。
如果系统上有ssh-copy-id
工具,您可以使用该工具。它会将您的公钥复制到远程服务器,将其放在您的主文件夹 SSH 目录(~/.ssh/authorized_keys
中),并设置正确的权限:
ssh-copy-id example.com
下面的示例与前面的示例类似,我们使用密码进行身份验证,但是ssh.ClientConfig
被配置为使用ssh.PublicKeys()
作为身份验证功能,而不是ssh.Password()
。我们还将创建一个名为getKeySigner()
的特殊函数,以便从文件中加载客户端的私钥:
package main
import (
"golang.org/x/crypto/ssh"
"io/ioutil"
"log"
)
var username = "username"
var host = "example.com:22"
var privateKeyFile = "/home/user/.ssh/id_rsa"
func getKeySigner(privateKeyFile string) ssh.Signer {
privateKeyData, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
log.Fatal("Error loading private key file. ", err)
}
privateKey, err := ssh.ParsePrivateKey(privateKeyData)
if err != nil {
log.Fatal("Error parsing private key. ", err)
}
return privateKey
}
func main() {
privateKey := getKeySigner(privateKeyFile)
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(privateKey), // Pass 1 or more key
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", host, config)
if err != nil {
log.Fatal("Error dialing server. ", err)
}
log.Println(string(client.ClientVersion()))
}
请注意,您可以向ssh.PublicKeys()
函数传递多个私钥。它接受无限数量的密钥。如果您提供了多个密钥,并且只有一个密钥对服务器有效,则服务器将自动使用有效的一个密钥。
如果您希望使用相同的配置连接到多个服务器,这将非常有用。您可能希望使用 1000 个唯一的私钥连接到 1000 个不同的主机。不必创建多个 SSH 客户端配置,您可以重用包含所有私钥的单个配置。
要验证远程主机,在ssh.ClientConfig
中,将HostKeyCallback
设置为ssh.FixedHostKey()
,并将远程主机的公钥传递给它。如果您尝试连接到服务器,但它提供了不同的公钥,则连接将中止。这对于确保连接到预期的服务器而不是恶意服务器非常重要。如果 DNS 被破坏,或者攻击者成功执行 ARP 欺骗,则您的连接可能会被重定向,或者成为中间人攻击的受害者,但如果没有相应的服务器私钥,攻击者将无法模拟真实服务器。出于测试目的,您可以选择忽略远程主机提供的密钥。
此示例是最安全的连接方式。它使用密钥而不是密码进行身份验证,并验证远程服务器的公钥。
此方法将使用ssh.ParseKnownHosts()
。这使用标准的known_hosts
文件。known_hosts
格式是 OpenSSH 的标准格式。该格式记录在*sshd(8)*手册页面中。
请注意,Go 的ssh.ParseKnownHosts()
将只解析单个条目,因此您应该为服务器创建一个具有单个条目的唯一文件,或者确保所需条目位于文件顶部。
要获取远程服务器的公钥进行验证,请使用ssh-keyscan
。这将以known_hosts
格式返回服务器密钥,该格式将在以下示例中使用。记住,Gossh.ParseKnownHosts
命令只读取known_hosts
文件中的第一个条目:
ssh-keyscan yourserver.com
ssh-keyscan
程序将返回多个键类型,除非用-t
标志指定了一个键类型。确保您选择了具有所需密钥算法的密钥,并且ssh.ClientConfig()
已列出要匹配的HostKeyAlgorithm
。此示例包括所有可能的ssh.KeyAlgo*
选项。我建议您选择可能的最高强度算法,并且只允许该选项:
package main
import (
"golang.org/x/crypto/ssh"
"io/ioutil"
"log"
)
var username = "username"
var host = "example.com:22"
var privateKeyFile = "/home/user/.ssh/id_rsa"
// Known hosts only reads FIRST entry
var knownHostsFile = "/home/user/.ssh/known_hosts"
func getKeySigner(privateKeyFile string) ssh.Signer {
privateKeyData, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
log.Fatal("Error loading private key file. ", err)
}
privateKey, err := ssh.ParsePrivateKey(privateKeyData)
if err != nil {
log.Fatal("Error parsing private key. ", err)
}
return privateKey
}
func loadServerPublicKey(knownHostsFile string) ssh.PublicKey {
publicKeyData, err := ioutil.ReadFile(knownHostsFile)
if err != nil {
log.Fatal("Error loading server public key file. ", err)
}
_, _, publicKey, _, _, err := ssh.ParseKnownHosts(publicKeyData)
if err != nil {
log.Fatal("Error parsing server public key. ", err)
}
return publicKey
}
func main() {
userPrivateKey := getKeySigner(privateKeyFile)
serverPublicKey := loadServerPublicKey(knownHostsFile)
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(userPrivateKey),
},
HostKeyCallback: ssh.FixedHostKey(serverPublicKey),
// Acceptable host key algorithms (Allow all)
HostKeyAlgorithms: []string{
ssh.KeyAlgoRSA,
ssh.KeyAlgoDSA,
ssh.KeyAlgoECDSA256,
ssh.KeyAlgoECDSA384,
ssh.KeyAlgoECDSA521,
ssh.KeyAlgoED25519,
},
}
client, err := ssh.Dial("tcp", host, config)
if err != nil {
log.Fatal("Error dialing server. ", err)
}
log.Println(string(client.ClientVersion()))
}
请注意,如果使用证书,除了ssh.KeyAlgo*
常量外,还有ssh.CertAlgo*
常量。
现在我们已经建立了多种验证和连接远程 SSH 服务器的方法,我们需要让ssh.Client
开始工作。到目前为止,我们只打印出客户端版本。第一个目标是执行单个命令并查看输出。
创建ssh.Client
后,即可开始创建会话。客户端一次支持多个会话。会话有自己的标准输入、输出和错误。它们是标准的读写器接口。
要执行命令,有几个选项:Run()
、Start()
、Output()
和CombinedOutput()
。它们都非常相似,但行为有点不同:
session.Output(cmd)
:Output()
函数将执行该命令,并将session.Stdout
作为字节片返回。session.CombinedOutput(cmd)
:与Output()
相同,但同时返回标准输出和标准误差。session.Run(cmd)
:Run()
函数将执行该命令并等待其完成。它将填充标准输出和错误缓冲区,但不会对它们做任何事情。在调用Run()
(例如session.Stdout = os.Stdout
之前,您必须手动读取缓冲区或将会话输出设置为转到终端输出。如果程序退出时错误代码为0
,并且复制标准输出缓冲区时没有问题,则它将返回而不出错。session.Start(cmd)
:Start()
功能与Run()
类似,只是不会等待命令完成。如果要在命令完成之前阻止执行,必须显式调用session.Wait()
。这对于启动长时间运行的命令或希望对应用流进行更多控制非常有用。
一个会话只能执行一个操作。一旦您调用Run()
、Output()
、CombinedOutput()
、Start()
或Shell()
,您就不能使用该会话执行任何其他命令。如果需要运行多个命令,可以用分号将它们串在一起。例如,可以在单个命令字符串中传递多个命令,如下所示:
df -h; ps aux; pwd; whoami;
否则,您可以为需要运行的每个命令创建一个新会话。一个会话相当于一个命令。
下面的示例使用密钥身份验证连接到远程 SSH 服务器,然后使用client.NewSession()
创建会话。然后,会话的标准输出连接到我们的本地终端标准输出,然后调用session.Run()
,它将在远程服务器上执行命令:
package main
import (
"golang.org/x/crypto/ssh"
"io/ioutil"
"log"
"os"
)
var username = "username"
var host = "example.com:22"
var privateKeyFile = "/home/user/.ssh/id_rsa"
var commandToExecute = "hostname"
func getKeySigner(privateKeyFile string) ssh.Signer {
privateKeyData, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
log.Fatal("Error loading private key file. ", err)
}
privateKey, err := ssh.ParsePrivateKey(privateKeyData)
if err != nil {
log.Fatal("Error parsing private key. ", err)
}
return privateKey
}
func main() {
privateKey := getKeySigner(privateKeyFile)
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(privateKey),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", host, config)
if err != nil {
log.Fatal("Error dialing server. ", err)
}
// Multiple sessions per client are allowed
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
// Pipe the session output directly to standard output
// Thanks to the convenience of writer interface
session.Stdout = os.Stdout
err = session.Run(commandToExecute)
if err != nil {
log.Fatal("Error executing command. ", err)
}
}
在前面的示例中,我们演示了如何运行命令字符串。还有一个打开 shell 的选项。通过调用session.Shell()
,执行交互式登录 shell,加载用户拥有的任何默认 shell 并加载默认配置文件(例如,.profile
。对session.RequestPty()
的调用是可选的,但在请求 psuedoterminal 时,shell 的效果要好得多。您可以将终端名称设置为xterm
、vt100
、linux
或其他自定义名称。如果由于输出颜色值而导致输出混乱,请尝试vt100
,如果仍然不起作用,请使用非标准终端名称或您知道的不支持颜色的终端名称。如果不识别终端名称,许多程序将禁用颜色输出。某些程序在未知终端类型下根本无法工作,例如tmux
。
有关 Go 终端模式常数的更多信息,请访问https://godoc.org/golang.org/x/crypto/ssh#TerminalModes 。终端模式标志是 POSIX 标准,在RFC 4254、终端模式编码(第 8 节)中定义,您可以在中找到 https://tools.ietf.org/html/rfc4254#section-8。
以下示例使用密钥身份验证连接到 SSH 服务器,然后使用client.NewSession()
创建新会话。我们将使用session.RequestPty()
获得一个交互式 shell,而不是像前面的示例那样使用session.Run()
执行命令。来自远程会话的标准输入、输出和错误流都连接到本地终端,因此您可以像任何其他 SSH 客户端(例如 PuTTY)一样实时与它交互:
package main
import (
"fmt"
"golang.org/x/crypto/ssh"
"io/ioutil"
"log"
"os"
)
func checkArgs() (string, string, string) {
if len(os.Args) != 4 {
printUsage()
os.Exit(1)
}
return os.Args[1], os.Args[2], os.Args[3]
}
func printUsage() {
fmt.Println(os.Args[0] + ` - Open an SSH shell
Usage:
` + os.Args[0] + ` <username> <host> <privateKeyFile>
Example:
` + os.Args[0] + ` nanodano devdungeon.com:22 ~/.ssh/id_rsa
`)
}
func getKeySigner(privateKeyFile string) ssh.Signer {
privateKeyData, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
log.Fatal("Error loading private key file. ", err)
}
privateKey, err := ssh.ParsePrivateKey(privateKeyData)
if err != nil {
log.Fatal("Error parsing private key. ", err)
}
return privateKey
}
func main() {
username, host, privateKeyFile := checkArgs()
privateKey := getKeySigner(privateKeyFile)
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(privateKey),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", host, config)
if err != nil {
log.Fatal("Error dialing server. ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
// Pipe the standard buffers together
session.Stdout = os.Stdout
session.Stdin = os.Stdin
session.Stderr = os.Stderr
// Get psuedo-terminal
err = session.RequestPty(
"vt100", // or "linux", "xterm"
40, // Height
80, // Width
// https://godoc.org/golang.org/x/crypto/ssh#TerminalModes
// POSIX Terminal mode flags defined in RFC 4254 Section 8.
// https://tools.ietf.org/html/rfc4254#section-8
ssh.TerminalModes{
ssh.ECHO: 0,
})
if err != nil {
log.Fatal("Error requesting psuedo-terminal. ", err)
}
// Run shell until it is exited
err = session.Shell()
if err != nil {
log.Fatal("Error executing command. ", err)
}
session.Wait()
}
阅读本章之后,您现在应该了解如何使用 Go SSH 客户端使用密码或私钥进行连接和身份验证。此外,您现在应该了解如何在远程服务器上执行命令或如何开始交互式会话。
如何以编程方式应用 SSH 客户端?你能想到任何用例吗?您是否管理多个远程服务器?你能自动完成任何任务吗?
SSH 包还包含用于创建 SSH 服务器的类型和函数,但本书中没有介绍它们。阅读有关在创建 SSH 服务器的更多信息 https://godoc.org/golang.org/x/crypto/ssh#NewServerConn 以及中关于 SSH 包的更多信息 https://godoc.org/golang.org/x/crypto/ssh 。
在下一章中,我们将研究暴力攻击,在这种攻击中,密码被猜测,直到最终找到正确的密码。我们可以使用 SSH 客户端以及其他协议和应用执行暴力强制。继续阅读下一章,学习如何执行暴力攻击。