アオカケスの鳥かご

日々の出来事を綴っていきたい

AWSでCTFの大会を開催するまでの道のり

CTFの大会を開催するにあたり、色々工夫しなければならない部分がありました。

今後、CTFを開催しようと考えている方の手助けになれば幸いです。
※あくまでも1つの方法として読んで頂けたらと思います。

前提

  • Linuxの基礎知識がある
  • 既にドメイン(サブドメインを設定できる)がある
  • 既にAWSのアカウントを所持しており、インスタンスを立てて接続できる
  • 多少の課金を覚悟できる※数百円程度
  • 小規模大会(サークル内や団体など身内だけで行う程度)
  • CTFに必要なサーバーは1台で済ませる

CTFサーバー構築手順

  1. AWSインスタンスを立てる
  2. ドメイン(サブドメイン)の設定
  3. インスタンス内でパブリックDNSを取得するスクリプトの用意
  4. CTFdのインストール
  5. HTTPS化
  6. nginxでリバースプロキシを構築
  7. PHPの設定
  8. CTFdのセットアップ

AWSインスタンスを立てる

EC2は規模に応じてスペックを調整します。
今回は小規模で人数にするとせいぜい20人程度なので、そんなにスペックは要りません。

お金に余裕がある場合はスペックを高めにしてもいいかもしれませんが、私はあまり余裕があるとは言えないのである程度絞ります。

今回はt3を利用します。料金表(東京リージョン)はこちら。
※為替レートを指定できます
$1 =

スペック vCPU メモリ 料金(Hour) 料金(Day) 料金(Week) 料金(Month)
t3.nano 2 0.5GB 0.0068$
0.1632$
1.1424$
5.0592$
t3.micro 2 1GB 0.0136$
0.3264$
2.2848$
10.1184$
t3.small 2 2GB 0.0272$
0.6528$
4.5696$
20.2368$
t3.medium 2 4GB 0.0544$
1.3056$
9.1392$
40.4736$
t3.large 2 8GB 0.1088$
2.6112$
18.2784$
80.9472$
t3.xlarge 4 16GB 0.2176$
5.2224$
33.5568$
161.8944$
t3.2xlarge 8 32GB 0.4352$
10.4448$
73.1136$
323.7888$


見ての通り意外と安いです。

気にするべきところはメモリぐらいでしょうか。
ポートを指定して常時動かしておかなければならないような問題や、Web系の問題などが多い場合はメモリに少し余裕を持ったほうがいいでしょう。

今回はsmallにします。
問題が少ない場合はmicroで十分だと思われます。

smallでも1週間あたり500円もかかりません(ドル円で地味に変わります)。
注意点として、この金額はインスタンスが動いている間常に発生します。すなわち、サーバーを構築している期間などにもお金が掛かります。
料金は開催期間+αで計算しましょう。


それでは実際にインスタンスを立てていきます。

スペックは以下の通り。

AMI Ubuntu Server 18.04 LTS
インスタンスタイプ t3.micro
ストレージ 汎用SSD(gp2) 8GB


セキュリティグループでは以下のように設定します。

タイプ プロトコル ポート範囲 ソース
SSH TCP 22 マイIP ***.***.***.***
HTTP TCP 80 カスタム 0.0.0.0/0
HTTPS TCP 443 カスタム 0.0.0.0/0
すべてのTCP TCP 0-65535 カスタム 0.0.0.0/0

AWSではインスタンスの中でファイアウォールを設定してもセキュリティグループの設定が優先されるようです。
SSHなどで接続するときはセキュリティグループに自分のIPアドレスが設定されていなければ接続することすらできません。

ソケット通信を行うような問題を出題するときは使用するポートをセキュリティグループにも設定します。
ここではすべてのポートを開放していますが、セキュリティ的には使うポートだけ設定するべきでしょう。


ちゃんと設定しているはずなのに接続できないなどといったときはセキュリティグループの設定を確認してみたらいいかもしれません。

ドメイン(サブドメイン)の設定

AWSのサービスの1つであるRoute53を使ってサブドメインの設定を行います。

お名前ドットコムでドメイン管理している場合は以下のサイトが参考になります。
nori-life.com

難しいことは何もありません。マウスでカチカチするだけです。

なお、Route53を利用する場合は月額$0.5かかります。無料に等しいのでケチるところではありません。

インスタンス内でパブリックDNSを取得するスクリプトの用意

EC2はインスタンスを起動するとIPアドレスが変わります。
そのおかげでスクリプト内でパブリックDNSを直に書いていた場合、起動し直したときに都度書き換えなければならなくなります。

そこで起動したときにIPアドレスとリージョンを取得してファイルに残しておくことにします。

Pythonでサクッと書きます。

# get_ipv4.py
import subprocess

result = subprocess.check_output(['curl', '-q', 'http://169.254.169.254/latest/meta-data/public-ipv4']).decode().split('.')
result2 = subprocess.check_output(['curl', '-s', 'http://169.254.169.254/latest/meta-data/placement/availability-zone']).decode()[:-1]

with open('/home/ctf/PublicDNS', 'w') as f:
    f.write('ec2-{}-{}-{}-{}.{}.compute.amazonaws.com'.format(result[0], result[1], result[2], result[3], result2))

IPアドレスとリージョンを取得し、それらを組み合わせて得られたパブリックDNSをPublicDNSというファイルに保存しています。


とりあえずスクリプトを書いた段階で一度実行しておきます。

$ python3 get_ipv4.py


スクリプトを起動時に実行するようにcrontabに記述します。

$ crontab -e
--
@reboot python3 /home/ctf/get_ipv4.py


ドメインを指定するときはこのスクリプトが吐き出したファイルを参照するようにすることで、インスタンスを起動したときに毎回書き直す手間を省くことができます。

CTFdのインストール

TeraTermなどでSSHで接続します。
ユーザー名に「ubuntu」入力し、秘密鍵インスタンスを立てるときに設定した秘密鍵を選択します。


とりあえずCTF用にユーザーを作成し、sudoに入れておきます。

$ sudo adduser ctf
$ sudo gpasswd -a ctf sudo

ユーザーを変更してCTFdをインストールします。

$ su ctf
$ cd ~
$ sudo apt update
$ sudo apt upgrade
$ git clone https://github.com/CTFd/CTFd.git
$ cd CTFd
$ git checkout 75a9a5a6972b7f053a018a0845e0755894729801
$ ./prepare.sh


サーバーの設定を変更します。
serve.pyというファイルの最後の行を、先ほど保存したパブリックDNSを参照するように書き換えます。ポートはお好みで。

#app.run(debug=True, threaded=True, host="127.0.0.1", port=4000)
with open('/home/ctf/PublicDNS', 'r') as f:
    HOST = f.read()
app.run(debug=True, threaded=True, host=HOST, port=8000)


これでCTFdが動くようになったので、CTFdを起動します。

$ sudo python serve.py

CTFdを起動したらブラウザからアクセスしてみます。
なお、この段階ではポートを80番以外に変更している場合、URLでポート番号を指定する必要があります。

セットアップの画面が表示されればOKです。
f:id:aokakes:20190824184248p:plain

HTTPS化

HTTPS化する場合はLet’s Encryptで証明書を取得します。

基本的にはこのサイトの通りに進めていきます。
ox0xo.github.io


まずはnginxをインストールします。

$ sudo apt install nginx

新たにdefault.confというファイルを作成し、諸々を書いておきます。
※既にある場合は適宜書き換える

$ sudo vim /etc/nginx/conf.d/default.conf
--
server {
    listen       80;
    server_name  [自分のドメイン];

    location / {
        root   /usr/share/nginx/html;
    }
}

nginxを再起動します。

$ sudo systemctl restart nginx

ブラウザから設定したドメインにアクセスしてみます。
nginxのウェルカムページが表示されればOKです。

statusを確認しろとか出てきたときはどこかミスっています。書き間違えている部分が無いか確認してみてください。


Webサーバの用意が出来たのでLet’s Encryptで証明書を取得します。

$ sudo apt-get install letsencrypt
$ sudo letsencrypt certonly --webroot --webroot-path /usr/share/nginx/html -d [自分のドメイン]

途中で何か聞かれたりしますが、AgreeやらYesやら選択します。
/etc/letsencrypt/archive/に自分のドメインがあればOKです。

途中でErrorになってうまくいかない場合はnginxがちゃんと動いていない可能性があります。

nginxでリバースプロキシを構築

CTFdはHTTPで動作しているため、nginxが受け取ったリクエストをCTFdに転送するようにします。

新たにssl.confというファイルを作成します。

$ sudo vim /etc/nginx/conf.d/ssl.conf
--
server {
  listen  443;

  ssl                   on;
  ssl_certificate       /etc/letsencrypt/archive/[自分のドメイン]/cert1.pem;
  ssl_certificate_key   /etc/letsencrypt/archive/[自分のドメイン]/privkey1.pem;

  ssl_session_timeout   5m;

  ssl_protocols              TLSv1.2;
  ssl_ciphers                HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers  on;

  location / {
    proxy_set_header Host             $host;
    proxy_set_header X-Real-IP        $remote_addr;
    proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-User $remote_user;
    proxy_pass http://[自分のドメイン]:8000;
  }
}


default.confに、HTTPリクエストをHTTPSに書き換えるように追記します。

$ sudo vim /etc/nginx/conf.d/default.conf
--
server {
    listen       80;
    server_name  [自分のドメイン];
    rewrite ^(.*) https://[自分のドメイン]$1 permanent;

    location / {
        root   /usr/share/nginx/html;
    }
}

nginxを再起動し、CTFdを起動します。

$ sudo systemctl restart nginx
$ sudo python /home/ctf/CTFd/serve.py

ブラウザにアクセスしてみてHTTPSになっていればOKです。

どこもエラーを吐いていないのにHTTPSになっていないときはShiftキーを押しながらページを更新してみたり、キャッシュを削除してみたりしてください。

PHPの設定

今回は問題サーバーも兼ねるので、nginxでPHPが動くようにします。

ApacheではPHPをインストールするだけで簡単に動くようになりましたが、nginxではいくつか段階を踏まなければなりません。

まずはPHPの諸々をインストールします。

sudo apt install php php-fpm php-mysql php-gettext php-common php-mbstring

PHPの設定ファイルを弄ります。

$ sudo vim /etc/php/7.2/fpm/php.ini
--
#778行目あたりでコメントアウトされているので、外して書き換える
cgi.fix_pathinfo=0

nginxでPHPが動くように設定します。
しかし、CTFdが動いているため普通にPHPの設定をしただけではすべてのアクセスがCTFdに飛ばされてしまいます。

そこでURLに特定の文字列を含むときは普通にnginxでサイトを表示するときと同じような動きをするように設定します。

問題を置く予定なので「problems」にします。

$ sudo vim /etc/nginx/conf.d/ssl.conf
--
server {
    (略)
    
    location / {
        (略)
    }
    
    location /problems/ {
        root   /usr/share/nginx/html;
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        root           /usr/share/nginx/html;
        try_files $uri =404;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
    }
}

nginxを再起動します。

$ sudo systemctl restart nginx

PHPが動いているか確認するために以下のファイルを用意します。

$ sudo vim /usr/share/nginx/html/problems/index.php
--
<?php phpinfo(); ?>

ブラウザでアクセスして色々表示されればOKです。

CTFdのセットアップ

CTFdを普通に起動するとSSHで端末を開いている間しかアクセスすることができません。
そこでSSHを切断しても動かし続けるために、screenコマンドを使います。

$ screen
$ sudo python /home/ctf/CTFd/serve.py

CTFdが起動したら「Ctrl+A」のあとに「D」でデタッチします。
これで端末から抜けてもCTFdが動き続けます。

起動したscreenに戻りたいときはまず「screen -ls」で一覧を表示します。
f:id:aokakes:20190824203343p:plain

そして以下のコマンドを実行します。

$ screen -r [赤枠の番号]


CTFdを起動したらブラウザからアクセスします。
上からCTFのタイトル、Adminのユーザー名、メールアドレス、パスワード、CTFのモードです。CTFのモードはチーム戦か個人戦かで選ぶことができます。
f:id:aokakes:20190824184248p:plain

セットアップが完了するとCTFdのロゴなどが表示されます。
f:id:aokakes:20190824204040p:plain


右上のAdminから管理者用のページに移動します。
Challengesのページの+を押すと問題を登録することができます。
f:id:aokakes:20190824204226p:plain

まずは問題の種類を選びます。

「dynamic」か「standard」のどちらかを選択します。
dynamicは解いた人の数で点数が変動し、standardは指定した点数で固定です。

次に問題名、ジャンル、問題文、点数を入力します。

submitすると次のような画面になります。
f:id:aokakes:20190824204643p:plain

左らへんのFlagsやFilesなどから答えとなるFlagや配布するファイルを登録することができます。
また、MessageにはHTMLタグを利用することが可能です。

Stateでは問題の公開/非公開を指定できます。


問題を登録したら大会の開催期間を登録します。
Config > Timeで登録するページに移動します。
f:id:aokakes:20190824233305p:plain

最後にページ全体の装飾です。
トップページがCTFdのロゴのままだったりするので、この部分をオリジナルのものに変えたりしましょう。

なお、トップページの内容は Pages > All pages の index から編集できます。
f:id:aokakes:20190824233546p:plain
f:id:aokakes:20190824233623p:plain
f:id:aokakes:20190824233733p:plain

必要に応じて新たにページを作成することもできます。


編集が終わったらConfigのBackupからバックアップファイルをエクスポートしておきます。
f:id:aokakes:20190824233350p:plain

環境が変わってもこのファイルをインポートするだけで問題を含め全ての設定が復元できます。


これで準備は完了です。時間になれば問題が公開されて解けるようになります。