SSH介绍以及go客户端脚本实现

SSH介绍

SSH 协议介绍

此处主要介绍一下SSH协议的结构以及其安全性。

在日常使用中,SSH (Secure shell Protocol) 是我们经常会用到的一个命令。通过它我们可以便捷的控制远端电脑。
同HTTPS作比较的话,相同点是它们都是用于客户机与服务器之间进行加密通信的一种机制。不同点在于,HTTPS协议是基于PKI(Public Key Infrastructor),也就是我们常说的CA,证书中心。用户在访问HTTPS站点之前会先向可信的CA请求该站点的数字证书,进而获取其公钥,之后与目标站点建立非对称加密连接。
而SSH的连接方式是这样的:(1)远程主机收到用户的登录请求,把自己的公钥发给用户。(2)用户使用这个公钥,将登录密码加密后,发送回来。(3)远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录。
这个过程存在几个风险:

  1. 如果有人截获了登录请求,然后冒充远程主机,将伪造的公钥发给用户。方法是第一次登陆的时候客户端会显示一段公钥指纹,这个时候可以在客户端选择相信或者不相信,如果相信的话则建立连接。
  2. 回放攻击,攻击者截获了公钥以及公钥加密的密码之后,便可以绕过用户直接与服务端通信。防范这个的方式是通过Diffie-Hellman算法。

经典Diffie-Hellman算法的计算步骤如下:

  1. 双方共同选择一个大值素数作为种子值(seed value)
  2. 双方共同选择一个加密生成器(通常是AES),用于后续的数值操作
  3. 双方分别各自选择一个素数,该素数的值对对方保密,用于生成本次通讯的私钥(与SSH身份认证私钥无关)
  4. 双方分别用各自的私钥、共同的加密生成器、和共同的素数生成各自的公钥
  5. 双方将各自的公钥共享给对方
  6. 双方用各自的私钥和对方发过来的公钥生成另一个密钥。根据该算法,双方各自计算出来的两个密钥是完全一样的,即“共同的秘密”
  7. 该密钥被用于本次通讯所有内容的加密

这样的话就能够保证此次通信仅限于这两个人之间,攻击者无法从中途插入。

除了使用账号密码登陆以外,用户还可以直接使用密钥登陆,具体方式就是使用ssh-keygen命令创建一对公私钥,然后使用ssh-copy-id命令将公钥上传到服务器,就可以免密码登陆了。

从图中可以看出,同一个SSH连接以内可以创建多个channel,既可以用于传输文本命令,也可以用于传输文件。这也就可以理解了我们常用的SSH登陆工具只用登陆一次就可以打开多个SSH窗口了。
对应着程序中,每次连接返回一个ssh.Client类型对象,使用该对象可以创建多个ssh.Session对象。

代码实现

GO 客户端实现

该程序能够打开一个ssh窗口,绑定标准输入输出,可以直接与远端通信。
由于terminal.MakeRaw()的存在所以无法在windows下运行。

package main

import ( 
	"fmt"
	"log"
	"os"
	"time"
	"golang.org/x/crypto/ssh"
	"golang.org/x/crypto/ssh/terminal"
	"net"
)
func main() { 
	session, err := connect("root", "Mao12345", "106.14.142.162", 22)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()
	fd := int(os.Stdin.Fd())
	oldState, err := terminal.MakeRaw(fd)
	if err != nil {
		panic(err)
	}
	defer terminal.Restore(fd, oldState)

	
	session.Stdout = os.Stdout
	session.Stderr = os.Stderr
	session.Stdin = os.Stdin

	termWidth, termHeight, err := terminal.GetSize(fd)
	if err != nil {
		panic(err)
	}
	// Set up terminal modes
	modes := ssh.TerminalModes{
		ssh.ECHO:          1,     // enable echoing
		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
	}
	// Request pseudo terminal
	if err := session.RequestPty("xterm-256color", termHeight, termWidth, modes); err != nil {
		log.Fatal(err)
	}
	session.Run("/bin/bash")
	// session.Run("top") 执行单个命令
	// session.Run("ls /; ls /abc") 执行单个命令
}


func connect(user, password, host string, port int) (*ssh.Session, error) { 
	var (
		auth         []ssh.AuthMethod
		addr         string
		clientConfig *ssh.ClientConfig
		client       *ssh.Client
		session      *ssh.Session
		err          error
	)
	// get auth method
	auth = make([]ssh.AuthMethod, 0)
	auth = append(auth, ssh.Password(password))
 
	clientConfig = &ssh.ClientConfig{
		User:    user,
		Auth:    auth,
		Timeout: 30 * time.Second,
		//需要验证服务端,不做验证返回nil就可以,点击HostKeyCallback看源码就知道了
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			return nil
		},
	}
 
	// connet to ssh
	addr = fmt.Sprintf("%s:%d", host, port)
 
	if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
		return nil, err
	}
 
	// create session
	if session, err = client.NewSession(); err != nil {
		return nil, err
	}
 
	return session, nil
}

应用

实际上,自己去写一个GO的客户端或者服务器端并没有什么实用性,但是它可以帮助我们实现更多灵活的功能。之前有段时间我需要同时操作多台云主机进行网络实验,如果使用逐个SSH连接的话实在过于繁琐,而且容易出错。这个时候就可以用GO来写一个网络脚本了。
当然也有许多好用的集群SSH操作工具,比如cluster SSH,PSSH等等。它们的优点就是上手更快,并且具有回显,错误也比较少。但缺点可能就是不够灵活了,而且它们必须应用于一个节点都高度同质化的集群上,而自己写的脚本则可以写出很灵活的判断语句,这个大家也可以自行选择.
下面这个例子包含上传文件、执行命令、下载文件三步,为了更为清晰可读,对原代码进行了删减。

// 自动化执行服务器端的 cap.sh脚本
package main

import (
	"fmt"
	"log"
	"net"
	"os"
	"os/exec"
	"time"
	"sync"
	"strings"
	"path/filepath"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
	"golang.org/x/crypto/ssh/terminal"
)

var (
	wg sync.WaitGroup
)

func main() {

	// connecting
	client_A, err := connect("root", "Mao12345", "106.14.173.234", 22)
	if err != nil {
		log.Fatal(err)
		return
	}
	client_B, err := connect("root", "Lsc19940810", "106.14.9.185", 22)
	if err != nil {
		log.Fatal(err)
		return
	}
	fmt.Println("SSH connecting succeed!")


	// first, you need to upload scripts; 上传脚本
	wg.Add(1)
	go func(client *ssh.Client) {
		upload_file("/home/cap.sh", "/root/test.sh", client)
		wg.Done()
	}(client_A)
	wg.Add(1)
	go func(client *ssh.Client) {
		upload_file("/home/cap.sh", "/root/test.sh", client)
		wg.Done()
	}(client_B)

	wg.Wait()
	fmt.Println("Uploading completed!")

	// begin to send backgroung flow and capture pkgs 执行脚本,执行完成之后下载文件
	wg.Add(1)
	go func(client *ssh.Client) {
		exec_remote_cmd("sh /root/cap.sh", client, false, nil )
		download_file("/root/tmp.pcap", "/home/mao/pcapfiles/D-"+case_num+".pcap", client)
		wg.Done()
	}(client_D)
	wg.Add(1)
	go func(client *ssh.Client) {
		exec_remote_cmd("sh /root/cap.sh", client, false, nil )
		download_file("/root/tmp.pcap", "/home/mao/pcapfiles/A-"+case_num+".pcap", client)
		wg.Done()
	}(client_A)
	wg.Wait()
}

func exec_remote_cmd(cmd string, client *ssh.Client, interactive bool, env map[string]string) {
	session, err := client.NewSession()
	if err != nil {
		log.Fatal(err)
		return
	}
	defer session.Close()
	session.Stdout = os.Stdout
	session.Stderr = os.Stderr

	for k, v := range env {
		fmt.Println("Set "+k+" to "+v)
		if err := session.Setenv(k, v); err != nil {
			log.Fatal(err)
		}
	}
	if interactive {
		session.Stdin = os.Stdin
		fd := int(os.Stdin.Fd())
		oldState, err := terminal.MakeRaw(fd)
		if err != nil {
			panic(err)
		}
		defer terminal.Restore(fd, oldState)
		termWidth, termHeight, err := terminal.GetSize(fd)
		if err != nil {
			panic(err)
		}
		// Set up terminal modes
		modes := ssh.TerminalModes{
			ssh.ECHO:          1,     // enable echoing
			ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
			ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
		}
		// Request pseudo terminal
		if err := session.RequestPty("xterm-256color", termHeight, termWidth, modes); err != nil {
			log.Fatal(err)
		}
			
	}
	session.Run(cmd)
}

func download_file(remote_file string, local_file string, client *ssh.Client) {
	sftpClient, err := sftp.NewClient(client)
	if err != nil {
		log.Fatal(err)
		return
	}
	defer sftpClient.Close()

	fmt.Println("Downloading files...")
	srcFile, err := sftpClient.Open(remote_file)
	if err != nil {
		log.Fatal(err)
	}
	defer srcFile.Close()

	// var local_file = path.Base(remote_file)
	dstFile, err := os.Create(local_file)
	if err != nil {
		log.Fatal(err)
	}
	defer dstFile.Close()

	if _, err = srcFile.WriteTo(dstFile); err != nil {
		log.Fatal(err)
	}

	fmt.Println("download: copy file from remote server finished!")
}

func upload_file(local_file string, remote_file string, client *ssh.Client) {
	sftpClient, err := sftp.NewClient(client)
	if err != nil {
		log.Fatal(err)
		return
	}
	// 用来测试的本地文件路径 和 远程机器上的文件夹
	srcFile, err := os.Open(local_file)
	if err != nil {
		log.Fatal(err)
	}
	defer srcFile.Close()

	dstFile, err := sftpClient.Create(remote_file)
	if err != nil {
		log.Fatal(err)
	}
	defer dstFile.Close()

	buf := make([]byte, 1024)
	for {
		n, _ := srcFile.Read(buf)
		if n == 0 {
	 		break
		}
		dstFile.Write(buf)
	}

	fmt.Println("upload: copy file to remote server finished!")
}

func connect(user, password, host string, port int) (*ssh.Client, error) {
	var (
		auth         []ssh.AuthMethod
		addr         string
		clientConfig *ssh.ClientConfig
		client       *ssh.Client
		err          error
	)
	// get auth method
	auth = make([]ssh.AuthMethod, 0)
	auth = append(auth, ssh.Password(password))

	clientConfig = &ssh.ClientConfig{
		User:    user,
		Auth:    auth,
		Timeout: 30 * time.Second,
		//需要验证服务端,不做验证返回nil就可以,点击HostKeyCallback看源码就知道了
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			return nil
		},
	}

	// connet to ssh
	addr = fmt.Sprintf("%s:%d", host, port)

	if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
		return nil, err
	}

	return client, nil

}

参考链接 :

posted @ 2018-04-02 16:27  四度  阅读(1254)  评论(0编辑  收藏  举报