はじめに
みなさんはCTF(Capture The Flag)ってご存知でしょうか。
CTFを通して、セキュリティ上の問題点を体験しながら、学ぶことができます。
せっかくなら、自分で脆弱性のあるサーバをつくりながら問題を説いてみようと思い、
CTFの初歩的な問題のサーバを作りました。
注意点
性質上、サーバに脆弱性がありますので、
この記事の内容をそのまま、本番の環境で実装するようなことはしないでください。
前提
CTF(Capture The Flag)とは
問題の中から隠されたフラグを見つけ出し、得点を稼ぐ競技。
コンピューターセキュリティーに関する、さまざまな問題から出題される。
今回は、Webアプリケーションの脆弱性に関する、Webの分野を対象とします。
技術選定
- Docker
- MySQL
- Python
この問題サーバでのルール
- フラグの形式は
myctf{フラグ}
となります。 - サーバの中には3つのフラグが隠されています。
つまり、このWebシステム上にmyctf{フラグ}
の形式もデータが3つあるので見つけてくださいということです。
サーバを構築する
Docker Network
docker networkで、コンテナ間のネットワークを作ります。
$ docker network create ctf-network
Docker Compose
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
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
にはフラグを埋め込んでおきます。
順番通りに解くと、これが最後のフラグになります。
## flag.md
myctf{mission_complete}
設定ファイル
MySQLの設定ファイルを追加する。
[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ファイル
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の中に2つのフラグを埋め込んでいます。
1つ目のフラグは、usersテーブルはdelete_flagを使った論理削除のテーブルで、削除したことになっているデータの中に埋め込んでいます。
2つ目のフラグは、今回のシステム上参照されないflagテーブルの中に埋め込んでいます。
Python
CTFのWeb問題ではPHPが主に使われますが、職場のバックエンドの環境がPythonなので、せっかくなので、Pythonで実装しました。
地味にこれが大変でした。(先にPHPのシステムから作ってました)
実際のCTFでPythonが使われるのはフラグ取得のための自動化スクリプト等が多い気がします。
PythonでWebアプリケーションを作る場合、DjangoやFlask等のフレームワークを使わないと大変になりますが、脆弱性を作るために、フレームワークには頼らずにできる限り自作します。
DockerFile
FROM python:3.8
WORKDIR /app
CMD bash -c "pip install -r ./requirements.txt && python app.py"
EXPOSE 8000
Requirements Files
いくつかは外部ライブラリを使います。
Requirements Filesにインストールするライルラリを記述します。
Jinja2~=2.10
PyMySQL~=0.9
HTML構文をpythonでハードコーディングするの辛かったので、テンプレートエンジンとしてJinja2を使いまます。
実行ファイル
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ファイル
<!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
実行する
$ docker-compose up -d
コンテナが起動したら、Webブラウザからhttp://localhost
にアクセスする。
画像のようなページが表示されるはずです。
職種検索のテキストボックスに「エンジニア」と入力して、「検索」ボタンをクリックすると
検索されているはずです。
その他
今回は、SQLインジェクションの問題だけですが、
Web定番のクロスサイトスクリプティングや、その他多数の脆弱性もあります。
むしろ脆弱性だらけで、問題解く側からすると、何から手をつけていいのかわからなくなるかもしれません。
気が向いたら、クロスサイトスクリプティングの問題も作ろうかと思います。
最後に
セキュリティの勉強会が「不正指令電磁的記録に関する罪」になるかもということで、
中止になるなどことがありましたが( http://ozuma.sakura.ne.jp/sumida/2019/03/15/77/ )、
この記事自体が、脆弱性の解説として、
「不正指令電磁的記録に関する罪」にならないことを祈ります。
今回の完成形のリポジトリはこちら
同時平行で作成していた、PHPバージョンのリポジトリはこちら
go言語+PostgreSQLで作ったリポジトリはこちら
go言語は静的型付け言語ということで、誤った型でUNIONするとエラーになるので、テーブル定義の推測は難しくなります。
この問題の回答方法は、CTF初心者が問題サーバ(web)を構築してみた【回答編】 にて