Chatwork Creator's Note

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

読者になる

インターンシップ課題として CTF を作った

どうも、こんにちは。

この場では2回目になります。

田尻(@lmdexpr)です。

今回は Chatwork Advent Calendar 2022 5日目(土日を入れると7日目)の記事です。*1

最近は CTF やらプログラミングコンテスト熱が再燃しています。

あと、OCaml 5.0 がリリースされた(はず*2)です。

最高ですね。

今回は夏頃に作った CTF の話をします。

背景

ある日のこと。 面白そうだったので二つ返事でオッケーしました。

CTF って?

と、ここまで CTF を知っている前提で話を進めていましたが、それって一体何?という話を簡単にしておきたいと思います。

CTF (Capture The Flag) とは、一種の競技にあたります。 幾つかの種類がありますが、多いのは脆弱性のあるアプリやサーバーが運営側によって用意され、プレイヤーはそれらを攻撃し、Flag と呼ばれる文字列を探します。

常設のものもありますので、興味を持たれた方は是非やってみてくださいね。

ただし、絶対に許可のないサーバーに対する攻撃は行わないでください

違法行為に該当します。

本記事は違法行為を助長するものではありません。

writeup

CTF には writeup というコンテスト終了後に解説記事を書く文化があります。

今回はそれにならって、まず、この度に作成した問題の解説から始めてみようと思います。

環境

今回は最初に競技者(インターン生)にはあるプライベートレポジトリを clone してもらいました。

環境を用意できなかったので各自のローカルで docker を動かしてもらう、という運営側の都合です。*3

1st stage

docker を立ち上げ、「localhost:8080」にアクセスすると、上の画面が出てきます。

とりあえず ID と Password を入力するようなので適当に入力してみます。

ID の入力欄は数字しか入らないようで、Password の方は特にマスクもされないかなり怪しい感じのシステムです。

これは脆弱性があるに違いありません。

とりあえず開発者ツールを開いてみてみます。

どうやら「Sign in」ボタンを押すと、サーバーに問い合わせをしているようです。

ということは DB に問い合わせをしているのでしょうか?

例えばselect * from auth where id = $id and password = '$password'みたいな感じで。

エンジニアの皆さんならお気付きでしょうか?

この予想が正しいなら SQL インジェクション(SQLi)が成立するかもしれません。

つまり、上のクエリに入るであろう文字列を少し工夫することで正しいパスワードが分からずとも認証を突破してしまえるのでは、という訳です。

試してみましょう。

例えば、こうです。

Sing in !!

という訳で、無事に認証を突破し、次の画面へとやってきました。

仕組みが分からないという方はぜひ画像の入力を上で書いたクエリにそのまま書いたらどうなるか、想像してみてください。

2nd stage

さて、どうやらこのシステムは何かを検索できるようですね。

とりあえず何も入れずに検索してみましょう。

お、何やら hint が出てきました。

次の問題はXSSのようです。

つまり、入力が表示されることを利用して、悪意ある JavaScript を実行させる訳です。

ところで今回はどこにテキストが表示されるのか、先に確認しておきましょう。

なるほど。

検索文字列が検索窓の下に表示されるようですね。*4

では、XSS を試してみましょう。 意図通りに alert が動きました。

確かに XSS が出来てしまうようです。

では、ヒントにあった通り、hint 関数を動かしてみましょう。

という訳で実行してみると、開発者ツールの console に何やらメッセージが出ています。*5

「Look at robots.txt」とあります。

3rd stage

言われた通りに robots.txt を見てみましょう。

中身はこのようになっていました。

admin というパスが存在するようです。

見てみましょう。

背景が少し違うだけの admin ページが表示されました。

さて、そろそろ重要な情報が眠ってそうです。

とりあえず同じ UI ですから 1st stage と同じように SQLi を試してみます。

4th stage

なんと admin ページでも SQLi が成功し、admin/search というページに遷移しました。

一体これを作ったのは誰なんでしょうか。*6

こんなことでは管理者の大事な情報が盗まれてしまいます。

さて、2nd 同様にとりあえず普通に使ってみましょう。

この管理者は非常にセキュリティ意識が高いようです。

しかし、これは私達にとっても朗報ですね。

今回も SQLi が出来そうです。

しかも「informaion_schema」というやつがヒントになるようですよ?

information_schema については例えば https://dev.mysql.com/doc/refman/8.0/ja/information-schema-introduction.html が参考になるでしょう。

ざっくり言うと、データベース自体の情報も含めた色々な情報が詰まっているテーブルということです。

これを見ることが出来ればあらゆる情報が取得できそうですね。

きっと重要情報(Flag)もここにあるに違いありません。

では、どのようにこの情報を抜きましょうか?

答えは union です。

今回は検索のシステムですから、例えばselect * from books where name likes '$query'のようにクエリを発行してそうです。

私達が挿入できるのは $query の部分ですが、例えば’ union select table_name from information_schema.tables #のようなクエリを挿入するとどうでしょう。

試してしましょう。

ダメなようです。

何がいけなかったのでしょう。

union は前の結果と数と型が合っている必要があります。

id, name, summary, owner_idが検索結果に表示されていますから、おそらくこの四つ(か、それ以上)を select していることがわかります。

union したいならこれに合わせなくてはいけないのです。

じゃあ、頑張って合わせてみて……

いけました!

この中だと何だか lbdesk_secret という怪しいテーブルがあります。*7

こいつを union して中身をみてみましょう。

カラム名はまだ分かりませんが、これも information_schema にあります。

見てみましょう。

flag がいますね!

さあ、あともう一歩!

という訳で今回の flag を見つけることができました!*8

お疲れ様でした。

問題の整理

問題の内容は講師の西川彰さんが作ってくれました。

実際の内容はこんな感じ。*9

CTF 経験者がいる想定はなかったので、時間内に雰囲気が伝わる、が重要でした。

実装で困ったこと

実は今回実装することで幾つか困ったことがありました。

こういう情報はあまりネットにもないようですし、折角なので軽く紹介したいと思います。

SQLi 編

今回のサーバーサイドは Scala を用いるという条件がありました。

やはり、インターンシップの業務も Scala を用いたものですからある意味当然です。

簡単な API サーバーがあれば良いということでライブラリは以下を選定しました。

  • Akka HTTP
  • circe
  • ScalikeJDBC

ここに一つ、ハマりポイントがあったのです。

ここでコードの一部を紹介しましょう。

  def authenticate(id: UserAccount.RawID, password: UserAccount.RawPassword): Option[UserAccount] =
    NamedDB("admin") readOnly { implicit session =>
      val query = s"select * from admin_users where id = $id and password = '$password'"
      val rs    = session.connection.prepareStatement(query).executeQuery()
      Option.when(rs.next()) { UserDAO.from(rs).toUserAccount }
    }

……お分かり頂けたでしょうか。

ScalikeJDBC を触ったことのない方に向けて、ちゃんと活用した場合のコードも掲載します。

  def authenticate(id: UserAccount.RawID, password: UserAccount.RawPassword): Option[UserAccount] =
    sql"select * from $userTableName where id = $id and password = $password"
      .map(DAO.User.apply)
      .single()
      .apply()
      .map(_.toUserAccount)

ScalikeJDBC というライブラリはその名の通り JDBC ラッパーです。

なんとこのコード、埋め込まれた変数をサニタイズまでしてくれます。

してくれちゃうんです。

これを回避するのに、かなり苦労しました。

ScalikeJDBC で利用されるコードは 1.5.1 *10以来、必ずサニタイズされるようになっており、ライブラリを利用する以上どうやっても SQLi が起きないようになっているのです。

ライブラリすごい!

結果、ラップされているjava.sql.Connectionを何とか取り出し、生文字列を組み立て、それをprepareStatementに食わせて……という具合にわざわざ SQLi を起こさせるコードを書きました。

この件から得られる教訓は「最新のライブラリを使え」ということになります。

XSS 編

XSS にも困ったことがありました。

今回、フロントエンドに関しては特別な制約がなかったので生の HTML+JS を用いました。

しかし、<script>alert(1)</script>のような入力があった場合に API を叩いてデータを取得し、表示するというような「ページロードが発生しない形に作っていた」ため、innerHTML に値を入れただけでは XSS が発火しなくなっていました。

フロントエンドに自信ないニキこと僕はおかしいなー、前はこれでいけた気がするなーと思いながらも、どうにも上手くいかないため、ここは講師の西川さんに引き継いでもらうことにしました。

<img src=1 onerror=“alert(1)”> みたいな入力も一般的な想定解であり、これなら発火するのですが、講義資料ではより基本的(?)な<script>alert(1)</script>で XSS が発火することを伝えたかった事情もあり、どちらもできるように下記のようなコードが作られました。

script_start_pos = book_name.indexOf('<script>', 0)
if (script_start_pos > -1) {
        document.getElementById('query').innerHTML = ''
        script_end_pos = book_name.indexOf('</script>', 0)
        var script = document.createElement('script')
        script.innerHTML = book_name.substring(script_start_pos + 8, script_end_pos)
        document.getElementById('query').innerHTML += book_name.substring(0, script_start_pos)
        document.getElementById('query').appendChild(script)
        document.getElementById('query').innerHTML += book_name.substring(script_end_pos)
} else {
        document.getElementById('query').innerHTML = book_name
}

XSS させるためだけのコードです!

世の中のシステムは大変堅牢に出来ていて安心ですね!

その他

細かい点では docker であったり、nginx であったりと詰まったこともありましたが、セキュリティ的な詰まりどころはあまりなかったように思います。

単純に勉強になりました。

完走した感想

さて、完走した感想ですが、大変貴重な体験ができました。

僕自身は CTF をやるのが好きで、学生の頃からちょこちょこやりつつ、それでも然程真剣に取り組んできた訳でもないので未だに初心者枠から抜け出せない、そんな感じです。

それでも色々知識はついていて、それが活かせたのかな?と思っています。

結果的にアンケートの満足度も100%と、インターン生からも好評だったようで*11良かったです。

今回の1ネタ

解説してしまうと来年使えないのでは?と悩んでいた僕に講師担当の西川さんがズバッと言ってくれました。

ありがとうございました。

かっこいい西川さん

終わりに

明日は cw-suetake の記事です。

楽しみですね。

いつもの

インターン全体の雰囲気を知りたい方は以下のレポート記事をご覧ください。

note.com

note.com

24卒本選考も始まってます!

CTF に自信がある方も、自信がない方もぜひ!

hrmos.co

*1:2年も記事書いてないですね

*2:12/6 現在のマイルストーンは 100% ですが、リリースはまだのよう

*3:ソースコードとかも配るので答えは探せば見えちゃう訳ですが、今回はコンテストではないので

*4:完全に余談ですが、L'Arc〜en〜Ciel には ' が含まれていますので、SQLi が出来るのか試す時に使われるとか使われないとか

*5:ちなみにこの問題は先ほどのヒントを見ずともソースコードを読むことでこのメッセージと hint 関数が暗号化されているのを発見できました

*6:僕です

*7:lbdesk は今回のシステムの名前でした

*8:今気付きましたが flag の文字列が誤字っていて恥ずかしい

*9:ここで工数を聞かれていて「大体10時間くらいっすかね〜」とかヘラヘラ答えました。実際はもっとかかったと思います

*10:かなり古いバージョンです

*11:当日は普通に仕事してたので西川さんに任せきりでした