CTFの作り方 ~How to create CTF~
はじめに
どうものりちゃんです。今年は阪神タイガースが優勝して素晴らしい一年でしたね。
もう今年も終盤ということで私にもアドベントカレンダーが回ってきました。
この記事を書いているときはちょうどセキュリティのイベントに参加していたので2023年を締めくくるべくCTFを作ることにしました。
0. 事前準備
事前にDockerとGitをインストールしておく必要があります。
1. スコアサーバの構築
今回は、高機能かつ簡単に構築できることから、スコアサーバーはCTFdを使うことにしました。
1.1 CTFdの構築
1.1.1 インストール・立ち上げ
CTFdをgit clone
して、試しに動かしてみましょう。
% git clone https://github.com/CTFd/CTFd.git scoreserver
% cd scoreserver
% docker-compose up --build
http://localhost:8000
にアクセスしてみます。
すると、上のようになり、/setup
にリダイレクトされ、セットアップ画面になりました。
1.1.2 スコアサーバーの設定
- Challenge Visibility: 問題を見る権限
今回はPrivateを選択 - Account Visibility: (他人の)アカウントを見る権限
今回はPublicを選択 - Score Visibility: (他人の)スコアを見る権限
今回はPublicを選択 - Registration Visibility: アカウント登録
今回はPublicを選択
- メールの確認: 登録時にメールを確認するか
今回はDisableを選択 - Teamsize: チームの最大人数
今回は1とした
これでスコアサーバーの設定は完了です。
1.1.3 問題の投稿
試しにWelcome問題を作成してみましょう。
-
localhost:8000/admin/challenges
にアクセスします。
Challengesの横にある"+"をクリックします -
ChallengeのTypeを設定します。今回はStandardを選択
Standard: 1問の点数を固定する方式
Dynamic: 正答者が多いほど点数が下がる方式 -
オプションやFlagを入力
今回は次のように選択
Flag: Flagを入力します
Case Sensitive(insenseitive): Flagの大文字小文字を区別する(しない)
Files: ファイルを選択(今回はファイルはないので省略)
State: 表示する状態か非表示の状態かを選択。今回はもちろんVisible。
最後にFinishを押すと問題は作成されます。
2. 問題の作成
2.1 どんなCTFにするか
どんなCTFにしたいか、コンセプトをある程度決めておきましょう。
具体的には、どんなスタイル(面白さ重視or難易度重視)で、どんなターゲット(CTF初めてor初級者or中級者or上級者)に向けての問題なのか、をある程度決めておくと良いでしょう。
2.2 どんな問題を作るか
どんな脆弱性を使ってどんな攻撃シナリオにするのかをある程度考えましょう。例えば、SQLインジェクションの中でもどこにSQLインジェクションの脆弱性があるかによってFlagを入手する難易度は変わります。
なので、「ログイン画面にSQLインジェクションを仕込んでパスワードをFlagにしよう」くらいで考えておきましょう。
どんな脆弱性を使うかは直近に話題になった脆弱性を使ってみたり、シンプルに好きな脆弱性を使ってみたりすると良いかもしれません。
私は、特定のフレームワークやライブラリを知らなければ解けない問題ではなく、一般的な脆弱性についての知識があれば解ける程度の問題で、ターゲットは初級者〜中級者というコンセプトで問題を作ることにしました。
2.3 問題の実装
注意:今回のCTFでは、Webのみとしました。
(Webくらいしかわからないため)
2.3.1 Web問
実装
先ほど考えたSQLインジェクションの問題を実装してみましょう。コンセプトは「ログイン画面にSQLインジェクションを仕込んでパスワードをFlagにしよう」でしたね。
注意: 問題サーバーとDockerのポートが競合しないようにDockerfileを書くようにしてください
では、実装してみましょう。今回は、WebアプリケーションをFastAPIとJinja2で、データベースをSQLiteを使用することにしました。
@app.post("/login")
async def login(request: Request, db: Session = Depends(session), username: str = Form(...), password: str = Form(...)):
print(username, password)
hashed_password = hashlib.sha256(password.encode()).hexdigest()
query = text(f"SELECT * FROM users WHERE username='{username}' AND password='{hashed_password}'")
result = db.execute(query).first()
if result:
return templates.TemplateResponse("success.html", context={"request": request, "message": "Login successful"})
else:
return templates.TemplateResponse("failed.html", status_code=401, context={"request": request, "detail": "Login failed"})
脆弱な部分はこのように実装しています。想定解としては、passwordパラメータにSQLクエリがInjectionできることを利用し、ブラインドSQLインジェクションを利用してadminのパスワードであるFlagを窃取されることを想定しています。('or '1'='1'--で解けると楽しくないよね!)
デバッグ
- SQLインジェクションを起こすことができるか
- パストラバーサルなど、想定外の解法を使うことができないか
を確認しました。想定外の解法を使って解くことができるのもCTFの醍醐味ですが、あまりに簡単にFlagが入手できてしまうような解法(パストラバーサル, etc.)を実行できないことを確認します。
今回は、PasswordのFormはSHA256でHash化されてDBに格納されているのでUsernameのFormからSQLインジェクションを起こせることを確認できました。
また、パストラバーサルは実行できないことを確認しました。(他に何か簡単にFlagが入手できる別解などあれば教えてください)
デプロイ
今回は無料で手軽にデプロイできるという観点からrender.comを採用しました。
render.comはGithubにプッシュすれば自動でデプロイしてくれるので非常に簡単です。
これで問題サーバーは完成です。
問題サーバーへの追加
3. スコアサーバーのデプロイ
あまりにも時間がなかったため(言い訳)、render.comを利用しました。時間が空いたらAWSなりGCPなりにデプロイしたものを投稿するのでしばしお待ちを。。。
ということでよければ覗いていってください
4. 最後に
CTF遊ぶのも楽しいですがシナリオ考えながら作るのも楽しいです!
デプロイがだいぶ雑になってしまったのでデプロイだけまた記事書きます。(多分年明けになる。。。)では、良いお年を!!!