CTF初心者が問題サーバ(web)を構築してみた【問題編】

## はじめに
みなさんはCTF（Capture The Flag）ってご存知でしょうか。
CTFを通して、セキュリティ上の問題点を体験しながら、学ぶことができます。
せっかくなら、自分で脆弱性のあるサーバをつくりながら問題を説いてみようと思い、
CTFの初歩的な問題のサーバを作りました。

## 注意点
性質上、サーバに脆弱性がありますので、
この記事の内容をそのまま、本番の環境で実装するようなことはしないでください。

## 前提

### CTF（Capture The Flag）とは
問題の中から隠されたフラグを見つけ出し、得点を稼ぐ競技。
コンピューターセキュリティーに関する、さまざまな問題から出題される。
今回は、Webアプリケーションの脆弱性に関する、Webの分野を対象とします。

### 技術選定
- Docker
- MySQL
- Python

### この問題サーバでのルール
- フラグの形式は`myctf{フラグ}`となります。
- サーバの中には3つのフラグが隠されています。

つまり、このWebシステム上に`myctf{フラグ}`の形式もデータが3つあるので見つけてくださいということです。

## サーバを構築する

### Docker Network
docker networkで、コンテナ間のネットワークを作ります。

``` bash
$ docker network create ctf-network
``` 

### Docker Compose
```yaml:docker-compose.yml
version: '3.3'
services:
  app-python:
    container_name: app-python
    build:
      context: ./python
      dockerfile: Dockerfile
    tty: true
    volumes:
      - ${PWD}/python/src:/app
    ports:
      - "80:8000"
    networks:
      - ctf-network    
  db1:
    container_name: mysql
    build:
      context: ./mysql
      dockerfile: Dockerfile
    environment:
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=password
    volumes:
      - ./mysql/mysql_conf:/etc/mysql/conf.d
      - ./mysql/initdb.d:/docker-entrypoint-initdb.d      
    networks:
      - ctf-network
networks:
  ctf-network:
```
user、passwordは適当に変更しておいてください。

### MySQL
CTFではSQLiteが手頃でよく使われるようですが、Dockerを使って構築するので、MySQLにしました。

#### DockerFile

```dockerfile:mysql/Dockerfile
FROM mysql:8.0
ADD ./ctf/flag.md /var/ctf/flag.md
RUN mkdir /var/log/mysql
RUN chown mysql:adm /var/log/mysql
```

#### フラグが記述されているファイル

`/var/ctf/flag.md`にはフラグを埋め込んでおきます。
順番通りに解くと、これが最後のフラグになります。

```markdown:mysql/ctf/flag.md
## flag.md
myctf{mission_complete}
```

#### 設定ファイル

MySQLの設定ファイルを追加する。

```config:mysql/mysql_conf/custom.cnf
[mysqld]
default_authentication_plugin=mysql_native_password
character-set-server=utf8
collation-server=utf8_general_ci
general_log=ON
general_log_file=/var/log/mysql/query.log
secure-file-priv = ""

[client]
default-character-set=utf8
```
ファイルにアクセスさせるので`secure-file-priv`の設定を追加する。

#### データベース初期化のSQLファイル

```sql:mysql/initdb.d/init.sql
SET CHARSET UTF8;

DROP DATABASE IF EXISTS ctf_db;
CREATE DATABASE ctf_db;
USE ctf_db;
DROP TABLE IF EXISTS users;
 
CREATE TABLE users (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(30) NOT NULL,
    last_name VARCHAR(30) NOT NULL,
    job VARCHAR(30) NOT NULL,
    delete_flag BOOLEAN NOT NULL DEFAULT FALSE
)DEFAULT CHARACTER SET=utf8;
 
INSERT INTO users (first_name, last_name, job) 
    VALUES 
    ("太郎", "山田", "サーバーサイドエンジニア"),
    ("次郎", "鈴木", "フロントエンドエンジニア"),
    ("三郎", "田中", "インフラエンジニア"),
    ("花子", "佐藤", "デザイナー");
INSERT INTO users (first_name, last_name, job, delete_flag)
    VALUES ("一郎", "渡辺", "myctf{scf_sql_injection_flag}", TRUE); /* フラグ1つ目(同じテーブル) */

DROP TABLE IF EXISTS flag;

CREATE TABLE flag (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    flag VARCHAR(60) NOT NULL,
    create_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    update_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)DEFAULT CHARACTER SET=utf8;

INSERT INTO flag (flag)
    VALUES ("myctf{next_flag_[/var/ctf/flag.md]}"); /* フラグ2つ目(別のテーブル) */
```
Databaseの中に２つのフラグを埋め込んでいます。
１つ目のフラグは、usersテーブルはdelete_flagを使った論理削除のテーブルで、削除したことになっているデータの中に埋め込んでいます。
2つ目のフラグは、今回のシステム上参照されないflagテーブルの中に埋め込んでいます。

### Python

CTFのWeb問題ではPHPが主に使われますが、職場のバックエンドの環境がPythonなので、せっかくなので、Pythonで実装しました。
地味にこれが大変でした。(先にPHPのシステムから作ってました)
実際のCTFでPythonが使われるのはフラグ取得のための自動化スクリプト等が多い気がします。

PythonでWebアプリケーションを作る場合、DjangoやFlask等のフレームワークを使わないと大変になりますが、脆弱性を作るために、フレームワークには頼らずにできる限り自作します。

#### DockerFile

```dockerfile:python/Dockerfile
FROM python:3.8
WORKDIR /app
CMD bash -c "pip install -r ./requirements.txt && python app.py"
EXPOSE 8000
```

#### Requirements Files

いくつかは外部ライブラリを使います。
Requirements Filesにインストールするライルラリを記述します。

```plaintext:python/src/requirements.txt
Jinja2~=2.10
PyMySQL~=0.9
```
HTML構文をpythonでハードコーディングするの辛かったので、テンプレートエンジンとしてJinja２を使いまます。

#### 実行ファイル
```python:python/src/app.py
import socketserver
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs

import pymysql.cursors
from jinja2 import Template, Environment, FileSystemLoader

PORT = 8000

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        conn = pymysql.connect(host='mysql',
                    user='root',
                    password='password',
                    db='ctf_db',
                    charset='utf8',
                    autocommit=True,
                    cursorclass=pymysql.cursors.DictCursor)
        try:
            url = urlparse(self.path)

            if url.path == '/':
                params = parse_qs(url.query)
                query = params.get('q', [''])[0]
                
                with conn.cursor() as cursor:
                    sql = f"SELECT * FROM users WHERE delete_flag=FALSE AND job LIKE '%{query}%';"
                    cursor.execute(sql)
                    users = cursor.fetchall()

                env = Environment(loader=FileSystemLoader('.'))
                template = env.get_template('index.html')
                
                data = {
                    'query': query,
                    'users': users
                }
                disp_text = template.render(data)

                self.send_response(200)
                self.send_header('Content-type', 'text/html')
                self.end_headers()
                
                self.wfile.write(disp_text.encode('utf-8'))
            else:
                self.send_response(404)
                self.send_header('Content-type', 'text/html')
                self.end_headers()
        except Exception as e:
            print(e)

            self.send_response(500)
            self.send_header('Content-type', 'text/html')
            self.end_headers()

            self.wfile.write(str(e).encode('utf-8'))
        finally:
            conn.close()

with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
    print("serving at port", PORT)
    httpd.serve_forever()
```
user、passwordはDocker Composeで変更した値に直してしてください。
SQL文の生成では、論理削除ということで、`delete_flag=FALSE`の判定が入っています。

#### jinja2のtemplateファイル

```html:python/src/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>CTF 体験</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
  <div class="container">
    <form class="form-inline my-3">
      <div class="form-group mb-2">
        <input type="text" name="q" class="form-control" placeholder="職種検索" value="{{ query }}">
      </div>
      <button type="submit" class="btn btn-primary mb-2 mx-sm-4">検索</button>
    </form>
    {% if query != '' %}
      <p><span class="text-danger">{{ query }}</span>の検索結果</p>
    {% endif %}

    <table class="table table-bordered">
      <thead>
        <tr>
          <th scope="col">ID</th>
          <th scope="col">姓</th>
          <th scope="col">名</th>
          <th scope="col">職種</th>
        </tr>
      </thead>
      <tbody>
        {% for user in users %}
          <tr>
            <td>{{ user.id }}</td>
            <td>{{ user.last_name }}</td>
            <td>{{ user.first_name }}</td>
            <td>{{ user.job }}</td>
          </tr>  
        {% endfor %}
      </tbody>
    </table>
  </div>
</body>
</html>
```

## 起動して確認
これがディレクトリの構造になります。

```
├─ docker-compose.yml
├─ mysql
│   ├─ Dockerfile
│   ├─ ctf
│   │   └─ flag.md
│   ├─ initdb.d
│   │   └─ init.sql
│   └─ mysql_conf
│       └─ custom.cnf
└─ python
    ├─ Dockerfile
    └─ src
        ├─ app.py
        ├─ index.html
        └─ requirements.txt
```

実行する

``` bash
$ docker-compose up -d
``` 
コンテナが起動したら、Webブラウザから`http://localhost`にアクセスする。

![2019-12-04 12.06.27 localhost e3de80904c2e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/9d58852f-fc7d-2548-5329-cfcf11fe2138.png)

画像のようなページが表示されるはずです。

職種検索のテキストボックスに「エンジニア」と入力して、「検索」ボタンをクリックすると

![2019-12-04 12.06.52 localhost c4217ceed7b3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/2005f84b-8691-8284-423b-4722e6d098bc.png)

検索されているはずです。

## その他
今回は、SQLインジェクションの問題だけですが、
Web定番のクロスサイトスクリプティングや、その他多数の脆弱性もあります。
むしろ脆弱性だらけで、問題解く側からすると、何から手をつけていいのかわからなくなるかもしれません。
気が向いたら、クロスサイトスクリプティングの問題も作ろうかと思います。

## 最後に
セキュリティの勉強会が「不正指令電磁的記録に関する罪」になるかもということで、
中止になるなどことがありましたが（ http://ozuma.sakura.ne.jp/sumida/2019/03/15/77/ ）、
この記事自体が、脆弱性の解説として、
「不正指令電磁的記録に関する罪」にならないことを祈ります。

今回の完成形のリポジトリは[こちら](https://github.com/fiotto/ctf-server-web-for-python)
同時平行で作成していた、PHPバージョンのリポジトリは[こちら](https://github.com/fiotto/ctf-server-web-for-php)
go言語+PostgreSQLで作ったリポジトリは[こちら](https://github.com/fiotto/ctf-server-web-for-golang)
go言語は静的型付け言語ということで、誤った型でUNIONするとエラーになるので、テーブル定義の推測は難しくなります。

この問題の回答方法は、[CTF初心者が問題サーバ(web)を構築してみた【回答編】](https://qiita.com/fiotto/items/df6e62b9a25fd62c62fd) にて