昨今流行に合わせたWebサイトの作り方など聞くことがしばしありますが、流行以前にWebサイトはDBの設計だったりセキュリティ対策など様々な事をしなければなりません。
そこで今回はWebサービスにおける脆弱性を利用した攻撃と防ぎ方、そして不正アクセスしたらなぜバレるのかなど私の私見ですがお話ししていこうと思います。
想定読者
- これから企業で研修受ける人
- 情報系の大学生・専門学生
注意
ここに書いてある内容は不正アクセスを推奨するものではなく、不正アクセスはどうやって防ぐかをまとめたものです。
自作サイトや自社で作ったサイトのテスト以外で不正なアクセスやパケットキャプチャは普通に違法になりますのでご注意を。
既にある設計論
実は情報処理技術者試験で有名な(っていうとそれが本業みたいになってしまいますが違います)IPAが『安全なウェブサイトの作り方』というサイトを作ってあります。
ここには各種Webのインジェクションの種類や対策などがコードではありませんが記載されています。

よく情報処理技術者試験で見る内容ですね(自分は持っていませんけど)。
では対策をしたサイトとしていないサイトではどのように違うのか、パケットキャプチャソフトのWireSharkと共にダミーウェブサイトを作って試してみようと思います。
ちなみにこんな感じの学生用掲示板サイトです。

脆弱性対策なし
コード
使っているフレームワークはPythonのFlaskです。FlaskはDjangoと異なり機能ごとに実装しないといけないためこういう勉強にはもってこいですし、なおかつ他の言語のウェブフレームワークに乗り換えたときも同じ感覚で使えます(ただしPHPのデータベース接続は面倒です)。
このコードは一部抜粋です。
@app.route("/thread2", methods=["GET", "POST"])
def thread2():
if "email" in session:
if request.method == "GET":
thread_id = request.args.get("id")
addr = request.remote_addr
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("INSERT INTO log (ip, email, uri, method, time) VALUES (?, ?, ?, ?, ?)", (addr, session["email"], "/thread2?id=%s"%(thread_id), "GET", now))
con.commit()
con.close()
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("SELECT team, num FROM comment WHERE thread_id=%s"%(thread_id))
res = ""
for team, num in cur:
res += team +" "+ str(num) + "<br>\n"
con.close()
return render_template("thread2.html", res=res, id=thread_id)
elif request.method == "POST":
thread_id = request.form["id"]
team = request.form["team"]
num = request.form["num"]
addr = request.remote_addr
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("INSERT INTO log (ip, email, uri, method, time) VALUES (?, ?, ?, ?, ?)", (addr, session["email"], "/thread2{id=%s, team=%s, num=%s}"%(thread_id, team, num), "POST", now))
con.commit()
con.close()
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("INSERT INTO comment (thread_id, team, num) VALUES (%s, '%s', '%s')"%(thread_id, team, num))
con.commit()
con.close()
return redirect("thread2?id="+html.escape(thread_id))
else:
return redirect("login-select")
<a href="logout">ログアウト</a><br>
<a href="home">ホームへ</a><br>
<form action="thread2" method="POST">
班 名<input type="text" name="team"><br>
学生証番号<input type="text" name="num"><br>
<input type="hidden" name="id" value="{{ id | safe }}"">
<input type="submit" value="post">
</form>
{{res | safe}}
といった感じでWebプログラミング初心者がやりがちな文字列の連結をそのままHTMLやDBに反映するというヤバい事をしています。
ではこれの何がヤバいか説明していきます。
XSS(クロスサイトスクリプティング)
XSSは簡単に言うと入力欄して反映させるサイトのテキストやDBの文字列にJavaScriptのコードを埋めておくと入力者が考えたJavaScriptが実行されるような内容です。
例えば入力欄に
<script>alert("XSS");</script>
とでも打ち込んでみましょう。
するとこのように表示されます。

これをパケットキャプチャで見てみると

HTMLファイル内にJavaScriptが埋め込まれていることが分かると思います。
ではこれができると何ができるかについてですが、JavaScriptを使った悪用が代替できます。例えば意図しないページにジャンプさせたり、あるいはセッションキーの情報を外部のサーバにXMLHttpRequestで送信したりしてログイン情報を流出させることができます。
SQLインジェクション
SQLインジェクションはこれもまたテキストボックスにSQLでよく使う「'(文字列の終了)」や「 -- (コメントアウト)」や「;(処理の終了)」を入れる事で元のSQLと異なった動作をさせることができます。
例えば今回使うサイトでは二つのテキストボックスがあり、そこの一つに文字列を入れる事でできます。
例えば一つ目のテキストボックスに

', (SELECT passwd FROM users WHERE email LIKE '%aaa@gmail.com%')) --
という感じでクエリを作ります。
すると元の
INSERT INTO comment (thread_id, team, num) VALUES (%s, '%s', '%s')"%(thread_id, team, num)
が
INSERT INTO comment (thread_id, team, num) VALUES (%s, '', (SELECT passwd FROM users WHERE email LIKE '%aaa@gmail.com%')) -- ', '%s')"%(thread_id, team, num)
に変わり、「aaa@gmail.com」というユーザのパスワードを引き出す形になります。
これを実行するとこのようになります。

ここではパスワードをハッシュ値にしているので基本的に漏れることはあり得ませんが、もし平文でDBにパスワードを保存していた場合はこれで漏洩し、不正アクセスやっている人のパスワードリストに追加されるということが起こります(まあ最近はChromeが自動でパスワードを作ってくれますが少なくともこのサイトでは同じパスワードを使うことは危険になります)。
CSRF
これは中々聞き馴染みが無い言葉ではないかと思います。実際対策をしていないサイトも散見されます。
これはどういう攻撃かというと、Formタグのactionの部分に不正入力したいサイトのURLを入れて、攻撃対象者がログインした状態で罠サイトに誘導したときにワンクリックでデータが送信されてしまう攻撃手法です。
例えばこれを使うと何ができるかですが、中には皆さんも経験あるかもしれませんが、SNSや掲示板などで書いた覚えのない事が書かれているということがあります。また最悪の場合(多要素認証しているのでまず起きないと思いますが)金融機関への不正送金をする事が考えられます。
例えばこんな罠サイトを作ってみましょう。
<h1>景品が当たりました</h1>
今すぐ下のボタンを押して景品を獲得してください
<form method="POST" action="http://192.168.1.141/thread2">
<input type="hidden" name="id" value="395">
<input type="hidden" name="team" value="ネオ麦茶">
<input type="hidden" name="num" value="m9(^A^)">
<input type="submit" value="景品をもらう">
</form>

見た目がこんな感じなので分かりませんが、FormタグのHiddenに様々な良くない文字列が格納されていることが分かると思います。
そしてこれをクリックすると

こんな感じにワンクリックで書き込みがされてしまうという事が起きます。
脆弱性対策
ではこれらの対策にはどうすればいいか、Pythonのコードを基に説明していきます。
@app.route("/thread", methods=["GET", "POST"])
def thread():
if "email" in session:
if request.method == "GET":
thread_id = request.args.get("id")
addr = request.remote_addr
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("INSERT INTO log (ip, email, uri, method, time) VALUES (?, ?, ?, ?, ?)", (addr, session["email"], "/thread?id=%s"%(thread_id), "GET", now))
con.commit()
con.close()
token = secrets.token_hex()
session["thread"] = token
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("SELECT team, num FROM comment WHERE thread_id=?", (thread_id,))
res = ""
for team, num in cur:
res += html.escape(team) +" "+ html.escape(str(num)) + "<br>\n"
con.close()
return render_template("thread.html", res=res, id=thread_id, token=token)
elif request.method == "POST":
if request.form["thread"] == session["thread"]:
thread_id = request.form["id"]
team = request.form["team"]
num = request.form["num"]
addr = request.remote_addr
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("INSERT INTO log (ip, email, uri, method, time) VALUES (?, ?, ?, ?, ?)", (addr, session["email"], "/thread{id=%s&team=%s&num=%s}"%(thread_id, team, num), "POST", now))
con.commit()
con.close()
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("INSERT INTO comment (thread_id, team, num) VALUES (?, ?, ?)", (thread_id, team, num))
con.commit()
return redirect("thread?id="+html.escape(thread_id))
else:
return "<h1>不正なアクセスです</h1>"
else:
return redirect("login-select")
では一つずつ見ていきましょう
CSRF対策
この対策したWebサイトを表示したときのパケットキャプチャはこのようになっています。

よく見ると16進数の長い文字列があると思います。これはPOSTで書き込みを行う前のワンタイムパスワードになります。
これをランダムに自動生成してHTMLのHiddenとセッションに格納し
if request.form["thread"] == session["thread"]:
...
をすることで不正に外部から書き込みされることを防ぐことができます。
この文字列の生成方法ですが
import secrets
...
token = secrets.token_hex()
session["thread"] = token
return render_template("thread.html", res=res, id=thread_id, token=token)
とすることで16進数文字列がセッションとFormタグに格納されてPOSTのたびに認証されます。
すると外部サイトではランダムな文字列なので当てるのがまず困難なのと、そもそもその存在を知らないとリクエストにパラメータが無いのでエラーが起きます。

XSS対策
res += html.escape(team) +" "+ html.escape(str(num)) + "<br>\n"
ここでは文字列をそのまま結合せずに「html.escape(文字列)」を使って文字列を結合しています。
こうすると「<」「>」などのタグや「"」といった文字列をブラウザに表示することができるように変換されます。
では実際に見てみましょう。

と表示され、パケットキャプチャで中身を見ると

となっています。
SQLインジェクション対策
これはもうシンプルにDB系のライブラリには必ずプレースホルダというサニタイズツールが入っているのでそれを使います。
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("INSERT INTO comment (thread_id, team, num) VALUES (?, ?, ?)", (thread_id, team, num))
con.commit()
これで対策ができ、実際に先ほどの文字列を打ち込むと

こんな感じで打ち込んだSQLがそのまま表示されます。
ログの追跡
とまあここまで読んで防げていればと思う人もいるかもしれませんが、自分で組んでいるWebサイトのセキュリティが常に安全とは言えません。どこかで対策を怠っているかもしれない。そこでログの確認です。
cur.execute("INSERT INTO log (ip, email, uri, method, time) VALUES (?, ?, ?, ?, ?)", (addr, session["email"], "/thread{id=%s&team=%s&num=%s}"%(thread_id, team, num), "POST", now))
一例としてこんな感じにIPアドレスとIDとURLとメソッドと時間をログに格納します。
次にIPアドレスで検索できるようにします。
con = sql.connect("data.db")
cur = con.cursor()
cur.execute("SELECT ip FROM log GROUP BY ip")
for ip in cur:
res = res + html.escape(str(ip[0])) + "<br>"
con.close()
これでサーバにアクセスされたIPアドレス一覧が出ます。
この中で報告があったIPアドレスで検索をかけます。
するとこのように表示されます。


といった形で悪い入力をしているのを見つけることができますよね。
その他にも正規のログアウトやそのほか正規の手段を使わずにユーザが変わっていたらセッションキーハイジャックを疑う事ができます。
アカウント乗っ取りの監視にも役に立つのでこういうログは残しておきましょう。
まとめ
悪い事したらバレる。