/var/log/Sawada.log

SAINO中毒患者の備忘録。

Violent Python Chapter 1

f:id:takuzoo3868:20180522034938p:plain

はじめに

どうも久しぶりです.最近ラボで画像処理と暗号理論の勉強に明け暮れているtakuzooです.先日,ちょっと古いですがエグいタイトルの洋書を見つけました.

Violent Python: A Cookbook for Hackers, Forensic Analysts, Penetration Testers and Security Engineers

Violent Python: A Cookbook for Hackers, Forensic Analysts, Penetration Testers and Security Engineers

当然,古い資料ですから2.x系のコードが掲載されているのですが,各章のストーリーの面白さとctfの勉強になるかなと読み始めました.どうやら邦訳されておらず,更には紹介している記事も大して見当たらないので,勉強のアウトプットとしてブログに残すことにしました.

下準備

今回はpython3環境下で頑張っていくことにしました*1
プロジェクトディレクトリでvirtualenvを使ってpythonの仮想環境を整えます.

$ virtualenv [env_name]
$ source [env_name]/bin/activate
$ pip install -r requirements.txt

パッケージリストはここにあります.

github.com

実行環境は以下のとおりです.やられサーバーはvirtualbox上に立てて,ネットワークにホストオンリーアダプタを使います.

  • ホストOS: Mac OS X 64bit 10.13.4 17E199 IP: 192.168.2.102

  • ゲストOS: Damn Vulnerable Linux(Linux 2.6.20-BT-PwnSauce-NOSMP) IP: 192.168.56.102

  • ゲストOS:OWASP BWA(Linux 2.6.32-25) IP:192.168.58.3

第1章の内容

  • Pythonの紹介と変数,型,文字列,リスト,関数の説明

  • 標準ライブラリと組み込みモジュールによるバナーチェックの実装

  • Dictionary Password Cracker for UNIX-system

  • Zipfile Brute-Force Cracker

Vulnerability Checking script

ここではpythonの基本的な仕様紹介とともに,バナーチェックを実装し,事前にまとめたバナーリストに従って調査を行っています. 攻撃者にとっては,バナーチェックはポートスキャンの次にサーバー上で利用されているアプリケーションの名称やバージョン番号を調査する段階を指します. 書籍が用意したバナーリストは以下の通りです.

$ cat banners.txt
ProFTPD 1.3.1 Server
(vsFTPd 2.3.4)
CCProxy Telnet Service Ready
ESMTP TABS Mail Server for Windows NT
FreeFloat Ftp Server (Version 1.00)
IMAP4rev1 MDaemon 9.6.4 ready
MailEnable Service, Version: 0-1.54
NetDecision-HTTP-Server 1.0
PSO Proxy 0.9
SAMBAR Sami
FTP Server 2.0.2
Spipe 1.0
TelSrv 1.5
WDaemon 6.8.5
WinGate 6.1.1
Xitami
YahooPOPs! Simple Mail Transfer Service Ready
3Com 3CDaemon FTP Server Version 2.0

エピソードの紹介も特にないのでとりあえず,書籍のソースコードを自分が扱いやすいようにoptparseを導入しただけです. 気が向いたらこのセクションの紹介もポートスキャンと共に加筆しようと思います(5/26). optparseはdeprecatedなのでargparseへ変更しました.

#!/usr/bin/env python
import socket
import argparse
from colorama import Fore, Style


def get_banner(ip, port, timeout):
    socket.setdefaulttimeout(timeout)
    s = socket.socket()
    try:
        print("[*] Grabbing banner from {}:{}".format(str(ip), port))
        s.connect((ip, port))
        ans = s.recv(1024)
        print("{}[+]{} Connection to {}:{} is succeeded!".format(Fore.GREEN, Style.RESET_ALL, str(ip), port))
        print("{}[+]{} {}".format(Fore.GREEN, Style.RESET_ALL, str(ans)))
        return ans
    except Exception as e:
        print("{}[-]{} Unable to grab any information: {}".format(Fore.RED, Style.RESET_ALL, ip, port, e))
        return None


def check_vulnerabilities(banner, filename):
    """
    Check for known vulnerable services against a pre-defined list of banners.

    :param banner: Banner being checked; type: str
    :param filename: List of vulnerability banners
    """
    with open(filename, 'r') as f:
        for line in f.readlines():
            if line.strip('\n') in banner:
                print("{}[-]{} Server is vulnerable: {}".format(Fore.RED, Style.RESET_ALL, banner.strip('\n')))
            else:
                continue


def main():
    parser = argparse.ArgumentParser(usage="%(prog)s -n <network>")

    """
    options default
    ---------------
    network     : 192.168.58.x
    subnet start: 1
    subnet end  : 5
    ports       : telnet, ssh, smtp, http, imap and https
    timeout     : 2   
    """
    parser.add_argument("-n", "--network", help="specify network to search on",
                        dest="network",
                        default="192.168.58.X")
    parser.add_argument("-start", "--start_subnet", help="specify which subnet should the scan start",
                        dest="start_subnet",
                        type=int, default=1)
    parser.add_argument("-end", "--end_subnet", help="specify which subnet should the scan stop",
                        dest="end_subnet",
                        type=int, default=5)
    parser.add_argument("-p", "--port", help="specify list of ports, separed by comma",
                        dest="ports",
                        default="21, 22, 25, 80, 110, 443")
    parser.add_argument("-f", "--file", help="default file with list of vulnerabilities to compare",
                        dest="file",
                        default="banners.txt")
    parser.add_argument("-out", "--socket_timeout", help="default socket connection timeout",
                        dest="socket_timeout",
                        type=int, default=2)
    options = parser.parse_args()

    subnet = options.network.lower()
    subnet_string = subnet.replace("x", "{}")
    ip_list = map(lambda ip: subnet_string.format(ip), range(options.start_subnet, options.end_subnet))
    port_list = map(int, filter(None, map(lambda p: p.strip(), options.ports.split(","))))

    print("[*] Testing subnet of {} for {} ports: {}".format(subnet, len(list(port_list)), options.ports))
    for ip in ip_list:
        for port in options.ports.split(","):
            port_int = int(port)
            banner = get_banner(ip, port_int, timeout=options.socket_timeout)
            if banner:
                print("[*] Checking {}:{}".format(ip, port))
                check_vulnerabilities(str(banner), filename=options.file)
                print("[*] Nothing vulnerable server :)")


if __name__ == '__main__':
    main()

Unix Password Cracker

ソースコードの紹介にあたり,著者は次のエピソードを紹介しています.

CUCKOO'S EGG

CUCKOO'S EGG

本の著者 Clifford Stoll 概要は以下の通り.

  • ローレンス・バークレー国立研究所にシステム管理者として勤務

  • 国立の研究機関や軍関係組織,学術機関に侵入していたハッカー*2を発見

  • 彼は追跡の経緯を論文や書籍で紹介

論文については下の参考文献に挙げておきました.システムに侵入した攻撃者がどうやってログインしたのかについて,書籍では以下のように語っています.

  1. UNIX crypt algorithmを利用したパスワードファイルをDL

  2. 英単語のリストを作成しUnix Crypt()で暗号化

  3. パスワードファイルと照会し一致する単語を総当たりで検索

  4. 該当した英単語でシステムにログイン

なぜ,辞書攻撃が成立したのかは以下の通りです.

Confronting some of the victim users, he learned they had used common words from the dictionary as passwords (Stoll, 1989).

ここからは攻撃者の視点で当時のパスワードクラッキングを再現し紹介していきます.再現にあたり用意したpasswd ファイルは以下のとおりです.

$ cat /etc/passwd
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
victim:MIO4AzhnF78Q.:503:100:Victim:/home/victim:/bin/sh
root:DFNFxgW7C05fo:504:100:System Administrator:/root:/bin/bash
daemon:*:1:1:System Services:/var/root:/usr/bin/false
bob:HX9LLTdc/jiDE:503:100:User:/home/bob:/bin/sh
alice:HXEboeNSbpTUE:503:100:User:/home/alice:/bin/sh

まずは,UNIX/etc/passwd構造を少し解説します.

f:id:takuzoo3868:20180524231308p:plain
the typical format of a user’s account entry in the /etc/passwd file

1990年以前のほとんどのUNIXシステムでは,/etc/passwdに暗号化された形式でパスワードを格納するのが一般的でした. 格納データの内容は図の通りですが,特に言及する点があるとすれば,

  • パスワードは改良されたDESアルゴリズムと2文字のsalt*3によって暗号化される

  • パスワードの欄がもしxとなっている場合,データはetc/shadowに格納されている(読み取り専用)

  • UIDの0番はroot専用であり,一般ユーザには1から99が割当される.100から999はシステム管理者やシステムユーザ向けに予約されている

  • GECOSには氏名,建物,電話番号や連絡先などを格納していた

となります.etc/shadowについては補足で説明しますが,これから紹介するソースコードの様に 簡単にパスワードをクラックできてしまうので,その対策として導入されたものだと考えてよいと思います. では,pythonのcryptモジュールを使って暗号化されたUNIXパスワードハッシュを計算してみます.

$ python
>>> import crypt
>>> crypt.crypt("egg","HX")
'HX9LLTdc/jiDE'

crypt.crypt()はハッシュされたパスワードを文字列として返してくれます.よってこれを利用し,辞書リストと任意のsaltを渡してpasswdの単語を推測するスクリプトを書いてみました.

#!/usr/bin/env python
"""
Execute a standard dictionary attack.
Usage: python passwd_Crack.py <Hash Algorithm> <dictionary_file> <pass_file>
"""

import crypt
import hashlib
import argparse
from tqdm import tqdm


def test_passwd(crypt_pass, dictionary_filename, algo):
    """
    Test passwords depending on hashing algorithm.

    DES:
    strips out the salt from the first two characters of the encrypted password
    hash and returns either after finding the password or exhausting the words
    in the dictionary.

    SHA512:
    ID (A value of 1 denotes MD5; 2 or 2a is Blowfish; 3 is NT Hash; 5 is
    SHA-256; and 6 is SHA-512.), salt and hash separated by $ This function
    currently only supports SHA512.
    """
    if algo == ("des" or "DES"):
        salt = crypt_pass[:2]
        with open(dictionary_filename, "r") as f:
            for word in tqdm(f.readlines()):
                word = word.strip()
                crypt_test = crypt.crypt(word, salt)

                if crypt_pass == crypt_test:
                    print("[+] Found password: {}".format(word))
                    return word
        print("[-] Password not found.")
        return

    elif algo == ("sha512" or "SHA512"):
        salt = str.encode(crypt_pass.split("$")[2])
        with open(dictionary_filename, "r") as f:
            for word in f.readlines():
                word = str.encode(word.strip("\n"))
                crypt_word = hashlib.sha512(salt + word)
                if crypt_word.hexdigest() == crypt_pass.split("$")[3]:
                    print("[+] Found Password: {}".format(word.decode()))
                    return
    else:
        print("Supported hashing algorithms: des / sha512")
        exit(1)


def main():
    parser = argparse.ArgumentParser(
        usage="%(prog)s --algo <des/sha512> --unknown_passwords <file list of hashed passwords> --test_passwords <file list of possible passwords>",
        description="Execute a standard dictionary attack.")

    parser.add_argument("--algo", help="specify algorithm DES or SHA512",
                        dest="algo",
                        default="des")
    parser.add_argument("--unknown_passwords", help="specify file that contains list of unknown hashed passwords",
                        dest="unknown_passwords",
                        default="password.txt")
    parser.add_argument("--test_passwords", help="specify file that contains list of possible passwords",
                        dest="test_passwords",
                        default="dictionary.txt")
    options = parser.parse_args()

    with open(options.unknown_passwords) as unknown_passwords:
        for line in unknown_passwords.readlines():
            if ":" in line:
                user = line.split(":")[0]
                crypt_pass = line.split(":")[1].strip(" ")
                print("[*] Cracking Password For: {}".format(user))
                test_passwd(crypt_pass, options.test_passwords, options.algo)


if __name__ == "__main__":
    main()

書籍の内容から改良を加えていますが基本構造はmain()test_passwd()になります.

  • main()でパスワードファイルの入力,解析,データの分割を行いtest_passwd()の呼び出し

  • test_passwd()では暗号化されたデータからsaltを保存し,辞書リストとsaltを用いてcrypt.crypt()でパスワードハッシュを生成

  • 生成したパスワードハッシュとパスワードファイルのデータが一致した場合,クラックできたとして単語を出力

/etc/passwdpassword.txtとして,ソースコードを実行した結果は以下の様になります.

$ ./passwdCrack.py
[*] Cracking Password For: nobody
100%|██████████████████████████████████████████| 16/16 [00:00<00:00, 32109.50it/s]
[-] Password not found.
[*] Cracking Password For: victim
  0%|                                                                                                                                                      | 0/16 [00:00<?, ?it/s]
[+] Found password: 12345
[*] Cracking Password For: root
  0%|                                                                                                                                                      | 0/16 [00:00<?, ?it/s]
[+] Found password: youwin
[*] Cracking Password For: daemon
100%|██████████████████████████████████████████| 16/16 [00:00<00:00, 61965.71it/s]
[-] Password not found.
[*] Cracking Password For: bob
  0%|                                                                                                                                                      | 0/16 [00:00<?, ?it/s]
[+] Found password: egg
[*] Cracking Password For: alice
  0%|                                                                                                                                                      | 0/16 [00:00<?, ?it/s]
[+] Found password: parcel

辞書リストのデータを用いて,30年ほど前に侵入者がパスワードを解析した状況を再現する事ができました.先程も述べた通り,簡単にクラッキングが成立してしまうため,今現在etc/passwdへ暗号化パスワードが記載されているシステムは稀です.その点を補足したいと思います.

補足

1992年ごろから,UNIXのシステムに/etc/shadowが実装されました./etc/passwdがリモートアクセスのユーザを含めユーザ全員が閲覧可能で,片方向のハッシュ関数で暗号化されてるとはいえ,上記のように辞書攻撃で解析される問題が浮上したからです.古いシステムとの互換性を保つ,特に別のプログラムがUIDやGIDを読み取るために/etc/passwdを残したままパスワードデータを移行した形になります.また,/etc/shadowはroot権限のみ閲覧可能でDESからSHA-512へハッシュアルゴリズムが変更されました.上のソースコードではSHA-512でも対応できるように改良を加えています.

cookuop.co.uk

Zip-File Password Cracker

このセクションでもソースコードの紹介にあたり,著者は次の事件について述べていました.

news.cnet.com

事件の概要を説明すると,

  • 2007年テキサス州ブラウンズビル消防署へ社内での児童ポルノ閲覧の匿名通報があった

  • 捜査に協力したプログラマの Albert Castillo は仕事用PCのHDDを調査した所,パスワード付きのzip "Cindy 5" を発見した

  • 同氏は古典的手法である辞書攻撃を用いてzipファイルを解読した

  • 復号したところ児童ポルノ数点が見つかったので容疑者は起訴された

という内容になっています.そこで実際に Castillo 氏と同じようにzipファイルをBrute Force attackでクラックしてみようというのがこのセクションの趣旨らしい.作ってみたソースコードは以下の通り.

#!/usr/bin/env python
"""
Run a dictionary attack for cracking zip file passwords.
Usage: python zip_Crack.py <zip_filename> <dictionary_filename>
"""
import sys
import os
import zipfile
import argparse
from tqdm import tqdm
from threading import Thread


def extract_file(file, password):
    """
    Try to extract files from secured zip file. Print password if that works
    """
    try:
        zipf = zipfile.ZipFile(file)
        zipf.extractall(path=os.path.join(file[:-4]), pwd=str.encode(password))
        print("[+] Found password {}".format(password))
    except:
        pass


def main():
    parser = argparse.ArgumentParser(
        usage="%(prog)s --zipfile <secure zipfile> --test_passwords <file list of possible passwords>",
        description="Run a dictionary attack for cracking zip file passwords.")

    parser.add_argument("--zipfile", help="specify zip filename to crack",
                        dest="zipfile",
                        default="evil.zip")
    parser.add_argument("--test_passwords", help="specify file that contains list of possible passwords",
                        dest="test_passwords",
                        default="dictionary.txt")
    options = parser.parse_args()

    with open(options.test_passwords) as dictionary_file:
        for possible_password in tqdm(dictionary_file.readlines()):
            password = possible_password.strip()
            t = Thread(target=extract_file, args=(options.zipfile, password))
            t.start()


if __name__ == "__main__":
    main()

書籍の内容に準じて,zipfileモジュールを使用します.extractall() に辞書ファイルの単語リストをパスワードの候補として渡し, 誤ったパスワードの場合でもBad password for fileと表示せずに,続けて解析をするよう例外処理を行っています. 解読に成功した場合は,そのパスワードを表示してzipファイルを展開する,という手順です.

また,関数を利用してソースコードをモジュール化する利点として,

  • パフォーマンスの向上

  • 複数の候補を同時に試行できる実行スレッドを扱える

といった点を挙げています.次に解凍するzipや辞書ファイルの指定ですが,argparseモジュールを活用しています. 書籍のコードは引数のフラグを設定していますが,個人的に入力が面倒なので,オプションにデフォルト値を設定しました. 更に,このモジュールの利点はhelpを追加できますので,以下のようにスクリプトに使用方法を記述することができます.

$ ./zip_Crack.py -h
usage: zip_Crack.py --zipfile <secure zipfile> --test_passwords <file list of possible passwords>

Run a dictionary attack for cracking zip file passwords.

optional arguments:
  -h, --help            show this help message and exit
  --zipfile ZIPFILE     specify zip filename to crack
  --test_passwords TEST_PASSWORDS
                        specify file that contains list of possible passwords

では,先程使用したdictionary.txtを利用して,配布されているevil.zipを解凍してみます.

$ file evil.zip
evil.zip: Zip archive data, at least v2.0 to extract
$ ./zipCrack.py
100%|██████████████████████████████████████████| 15/15 [00:00<00:00, 212.22it/s]
[+] Found password secret
$ tree evil
evil
└── evil
    ├── evil.jpg
    └── note_to_adam.txt

1 directory, 2 files
$ cat evil/evil/note_to_adam.txt
Sorry, you are too late - she ate the apple.
--------
[Image downloaded from http://farm3.staticflickr.com/2422/4424308439_7bd9e833d3_z.jpg under Creative Commons License]

zipのパスワードが secret であることはチュートリアルなので,スクリプトを実行するとすぐに解析できます.実際のzip形式ファイルの場合には,github上に有名なパスワードをまとめた辞書ファイル*4が転がっていますので,そうした辞書ファイルを使って解析することになるのだろうと思われます.他に挙げるならOpenWallなどが良いと思います*5.OpenWallは John the Ripper の開発元で,いい感じの辞書ファイルをサンプル提供しています.書籍では対象がzipでしたが,辞書攻撃で有名なツールにはTHC-HydoraやAircrack-ngなどがあります.

解凍後 evil の中身はイヴが禁断の果実に手を出してしまったよ...という設定ですね.とりあえずアダムは手遅れだったようです.

f:id:takuzoo3868:20180523014719j:plain

終わりに

研究の息抜きに読んだ資料だったのですが,思いの外面白かったのでこの調子で次の章も頑張っていきます.では,次の記事のアレです.

TmV4dENoYXB0ZXJQYXNzd29yZHs0Xzhja2Vyc30=

参考文献

fukurami.hatenablog.com

*1:書籍の内容を追うだけなら,dockerなりで2.x系の環境構築をする方が賢明かと思われます

*2:実際にはソ連国家保安委員会に雇われていた人物

*3:パスワードを暗号化する際に付与されるデータ

*4:GitHub - duyetdev/bruteforce-database: Bruteforce database

*5:当然,Brute Force attack なので時間はそれなりに掛かります