サイト内の現在位置

トレーニングコンテンツ:脆弱なAPIサーバー「Tiredful API」のご紹介

NECセキュリティブログ

2021年11月5日

NECサイバーセキュリティ戦略本部セキュリティ技術センターの中島健児です。本記事では、APIの脆弱性診断のスキル向上に役立つ学習コンテンツ「Tiredful API」をご紹介します。

Tiredful APIとは

Tiredful API new window[1]は、Siddharth Bezalwar氏が作成したツールで、GPL3.0で公開されています。本ツールは、APIを作成する開発者やセキュリティ技術者がAPIのセキュリティについて学べるように、あらかじめAPIアプリケーションに基本的な脆弱性を盛り込んで作られた学習用コンテンツ(いわゆるやられ環境)です。後ほど紹介するように、Tiredful APIは基本的な脆弱性を中心として作られたコンテンツであり、ヒントもありますので、APIのセキュリティについて基礎から学ぶための題材として取り組みやすいコンテンツだと思います。

Tiredful APIには以下の6つの脆弱性が含まれています。

  1. Information Disclosure
  2. Insecure Direct Object Reference
  3. Access Control
  4. Throttling
  5. SQL Injection
  6. Cross Site Scripting

APIによってはリクエスト時にBearerトークンが必要な場合がありますので、後述するトークン取得ページが用意されています。

Tiredful APIのセットアップ

それでは Tiredful APIのセットアップを行います。セットアップはgithub上のREADME.mdに従い行っています。
まず、レポジトリからクローンしてきます。
$ git clone https://github.com/payatu/Tiredful-API.git
$ cd Tiredful-API
Tiredful API を実行する方法はいくつかあるのですが、本記事ではDockerで実行する方法を選択します。
コンテナイメージをビルドします。
$ docker build -t tiredful .
コンテナを起動します。その際に8000番ポートをバインドします。
$ docker run -p 8000:8000 --name tiredful -it tiredful

上記のdocker runコマンドを実行するとTiredful APIが立ち上がりますので、ブラウザ等で http://localhost:8000/にアクセスして動作していることを確認します。以下のようなページが表示されるはずです。

Let’s Startボタンを押すとIntroductionページへと進み、APIのリクエストを送信する際の注意点が表示されます。
1.すべてのAPIは以下のACCEPTヘッダーを持つこと
Accept: application/json
2. POSTメソッドでのAPIリクエストでは以下のContent-Typeヘッダーを持つこと
Content-Type: application/json

いくつかのAPIではトークンが必要となり、User Tokenページで取得可能との説明があります。User Tokenページには2つのユーザークレデンシャルの記載がありますので、これらを使用して各ユーザーのBearerトークンを取得できます。認証が必要なリクエストでは、以下のようにトークンをヘッダーに付与してリクエストを送信します。
Authorization : Bearer < token >

Tiredful APIの各チャレンジに挑戦

Tiredful APIでは6つの脆弱性がチャレンジとして提示されています。各ページには、APIの仕様と見つけ出すべき脆弱性がAimとして記載してあります。

これ以降では各6つのチャレンジに挑戦します。なお本記事ではTiredful APIの脆弱性調査に BurpSuite Professional v2021.9.1 new window[2]を使用しています。

1. Information Disclosure

APIの仕様:

APIの機能:ISBN番号を元に本の詳細情報を返すAPI
パス メソッド リクエストボディ 備考
/api/v1/books/<ISBN>/ GET - -

チャレンジの内容:
スタックトレースの情報を得る

正常リクエストを実行:
<ISBN>に用意されていた値を入れてリクエストした結果は以下の通りです。

チャレンジに挑戦:
それでは、スタックトレースの情報を得るために、 まずは<ISBN>に適当な値を入れて確かめていきます。様々な入力値を使って試したところ、どうやら大文字のアルファベットを入れるとスタックトレースの内容がJSON形式で返されるようです。数値や小文字ではステータスコードが404 Not Foundとなり有効な応答はありませんでした。

以上のように、大文字のアルファベットを<ISBN>に入れることで、チャレンジの目的であるスタックトレースの情報が表示され、チャレンジクリアとなりました。

2. Insecure Direct Object Reference

APIの仕様:

APIの機能:生徒の試験結果を返すAPI
パス メソッド リクエストボディ 備考
/api/v1/exams/<exam_id>/ GET - -

チャレンジの内容:
認証した生徒とは別の生徒の試験結果にアクセスする

正常リクエストを実行:
まず、User Tokenページの”Get User Token”にて、user1であるbatmanのBearerトークンを取得します。Bearerトークンをヘッダーに含め、<exam_id>には数値”1”を設定し、Base64でエンコードした値である”MQ1==”の値を入れてリクエストを送信します。user1の成績が返ってきました。

チャレンジに挑戦:
「別の生徒の試験結果にアクセス」するというのが今回のチャレンジです。各生徒の試験結果は、<exam_id>に数値を入れて参照していることから、<exam_id>に別の数値を入れることで任意の生徒の試験結果が参照できないか確かめてみます。batmanのBearerトークンを使いつつ、 <exam_id>に数値1から1000までBase64エンコードした上で入れてリクエストしていきます。

すると<exam_id>が”NTY=”(56)と”OTM=”(93)の場合に他の生徒の試験結果が返ってくることがわかりました。

batman(user1)のトークンを使っているのですが、他生徒の試験結果が見えました。チャレンジクリアです。

3. Access Control

APIの仕様:

APIの機能:ブログアプリケーションの記事の作成、編集、閲覧を操作するAPI
パス メソッド リクエストボディ 備考
/api/v1/articles/<article-id>/ GET - 記事の閲覧用
/api/v1/approve-article/<article-id>/ GET - 記事の承認用(管理者のみ)

チャレンジの内容:
管理者しか実行できない操作を権限なしに実行する

正常リクエストを実行:
閲覧用のAPIの <article-id>に”4”を入れてリクエストした結果です。

記事のタイトルとコンテンツの内容(短いですが)が返ってきています。さて、レスポンスで気になるのはレスポンスヘッダーのAllowに含まれるDELETEの文字です。このAPIではDELETEメソッドが使用可能と考えられます。

チャレンジに挑戦:
正常リクエストを試した際に、レスポンスのAllowにDELETEメソッドの記述があったことから、メソッドをDELETEに変えてリクエストを送信してみます。するとレスポンスに"¥"IsAdmin header missing¥""というメッセージが返ってきました。

IsAdminヘッダーを付けてリクエストすると良さそうですが、IsAdminヘッダーに必要な値がわかりません。そこで、推測で1, true, True等の値にあたりを付けて試しました。すると、IsAdmin:Trueで画像のように"¥"Successfully deleted¥""と表示されたため、記事を削除できたようです。確認のため、GETメソッドでリクエストして記事の取得を試みた所、レスポンスが404 Not Foundとなり実際に削除されていることを確認できました。

IsAdmin:Trueヘッダーを付与してDELETEメソッドでリクエストすると、本来管理者向けの機能と考えられる記事の削除を行えたためチャレンジクリアです。

4. Throttling

APIの仕様:

APIの機能:PNR(予約番号)を基に予約情報を閲覧するAPI
パス メソッド リクエストボディ 備考
/api/v1/trains/ POST {"PNR": <pnr_number(string)> } -

チャレンジの内容:
レスポンスステータスコード429での応答をサーバーに強制させる

正常リクエストを実行:
リクエストボディの <pnr_number(string)>には本ページで提示されている数値を入れてリクエストした結果が以下です。予約情報が返って来ます。

チャレンジに挑戦:
レスポンスステータスコード429は、一般的には一定時間に多くのリクエストが発生すると返ってくるコードです。そこで短時間にたくさんのリクエストを送ってみます。
上記のリクエストをBurpSuiteを使って短時間に大量アクセスします。すると今回の場合、41リクエスト目から429が返ってくるようになり、チャレンジを達成できました。

5. SQL Injection

APIの仕様:

APIの機能:毎月のフィットネス活動の情報にアクセスするAPI
パス メソッド リクエストボディ 備考
/api/v1/activities/ POST {"month": <month(string)>} -

チャレンジの内容:
SQLiteデータベースのテーブル名を調べる

正常リクエストを実行:
本ページの例で示されているように、<month(string)>に”1”や”2”,”11”を入れた場合に、以下のようにレスポンスが返ってきます。

チャレンジに挑戦:
リクエストボディのパラメータである<month(string)>に対してSQLインジェクションを試みます。
データベースはSQLiteと分かっていますのでSQLiteの内部テーブルであるsqlite_masterを使用してテーブル名を取得することを狙います。
呼び出しの例:SELECT name FROM sqlite_master WHERE type='table'
続いて本APIの呼び出しで発行されるクエリーにより返却されるデータに、UNION句を使用してsqlite_masterテーブルの内容を結合して表示させることを狙います。UNION句でデータを結合するには、結合するカラム数を合わせる必要があります。元のクエリーにより返却されるカラム数は操作できませんから、UNION句で結合するデータのカラム数をnull(数値でも良いです)で合わせます。カラム数が合致していない場合は、一般的なエラーページやデータベースのエラー文の表示やレスポンスが返って来ない等の通常とは異なる挙動があるはずですので、エラーや挙動を見つつnullの数(=結合するデータのカラム数)を徐々に増やしていきます。今回の場合はカラム数が合致していないと” SELECTs to the left and right of UNION do not have the same number of result columns”という文が含まれるエラーが返却されます。以下の様にnullの数を増やしていきます。

“1 UNION SELECT name FROM sqlite_master WHERE type='table'”
“1 UNION SELECT null,name FROM sqlite_master WHERE type='table'”
“1 UNION SELECT null,null,name FROM sqlite_master WHERE type='table'”
・・・
“1 UNION SELECT null,null,null,null,null,null,name FROM sqlite_master WHERE type='table'”
//nullではなく任意の数値でも良いです
“1 UNION SELECT 1,2,3,4,5,6,name FROM sqlite_master WHERE type='table'”

nullを6個にするとレスポンスコード200で結果が返ってきます。UNION句で結合するデータのカラム数が元のクエリーから返却されるカラム数と合致したことがわかります。
上記、最後のパラメータでリクエストを送信するとJSONの各ブロックのuserにテーブル名が含まれて返って来ます。テーブル名取得できましたのでチャレンジクリアです。

6. Cross Site Scripting

APIの仕様:

APIの機能:広告の内容を投稿・閲覧するAPI
パス メソッド リクエストボディ 備考
/api/v1/advertisements/ GET - 広告の一覧を取得(トークンが必要)
/api/v1/advertisements/ POST {"headline": <headline(string)>,"info": <info(string)>, "price": <price(float(7,2))> } 広告を作成(トークンが必要)

チャレンジの内容:
クロスサイトスクリプティングの文字列を受け入れてしまうパラメータを探す

正常リクエストを実行:
広告の内容を登録するためにPOSTリクエストを送信します。本APIは事前にBearerトークンを取得してヘッダーに付与する必要があります。パラメータの”headline”と”info”には文字列、”price“には数字を入力します。

続いて、登録されている広告一覧を閲覧するために、GETリクエストを送信します。本APIは同様に事前にBearerトークンを取得してヘッダーに付与する必要があります。

POSTメソッドで登録した内容を取得・確認することができます。

チャレンジに挑戦:
クロスサイトスクリプティングの典型的な文字列である “<script>alert(‘ERROR!!’)</script>”という文字列がパラメータの値として受け入れられないかを試します。“headline”と”info”はどちらも文字列を値に取ることがわかっていますので、前述の文字列を入れてリクエストを送信してみます。

登録に成功したようです。登録した内容を確かめるためにGETメソッドでリクエストを送信します。

先ほど実行したクロスサイトスクリプティング用の文字列を持った広告データが登録されていることが確認できました。“headline”と”info”パラメータにクロスサイトスクリプティング用の文字列を入力できることがわかりましたので、チャレンジクリアです。

おわりに

本記事では、あらかじめAPIアプリケーションに基本的な脆弱性を盛り込んで作られた学習用コンテンツTiredful APIを紹介しました。Tiredful APIは各チャレンジに攻略目標が明示してあるため迷わず進めることができ、さらに攻略難易度も比較的容易です。ただし作り込みの問題でチャレンジとは別のエラーが発生する場合があることに注意が必要です(Djangoのエラー文等)。この点も含め、APIのセキュリティチェックの手法を学ぶための基礎コンテンツとして楽しみながら取り組めると思います。

参考情報

執筆者プロフィール

中島 健児(なかしま けんじ)
セキュリティ技術センター リスクハンティングチーム

リスクハンティングチームにて、NECがお客様へ納品するシステム・製品へのリスクアセスメント/脆弱性検査/ペネトレーションテストを通じて、安全・安心なシステム構築を支援する業務に従事
CISSP Associate、情報処理安全確保支援士(RISS) 保持

執筆者の他の記事を読む

アクセスランキング