場阿忍愚CTF Writeup

場阿忍愚CTFに参加しました。
結果は77位、次回はもっと時間作って頑張りたいです。

参加した記録を残す意味も込めて、超文章転送術(web問)400点問題の「Yamatonote」のWriteupを書いていきます。

超文章転送術 400 Yamatonote

どんな問題?

ノートを保存できるYamatonoteというサイトから、yamatoユーザーのノートを盗み見る問題。
特徴としてはそのノートをyaml形式でアップロードして保存できるところ。

方針

データベースのスキーマ情報を見ると、ノートを格納しているNoteテーブルは主キーが「ユーザー名」になっていてこの部分がyamatoになっているものを参照できれば盗み見れるんだろうなーと方針を立てる。

またソースコードを見ると、Noteテーブルの参照をSessionクラスの「userId変数」を利用してユーザーに対するノートを取得していることがわかった。
SessionクラスはデータベースのSessionテーブルを管理するクラス。SessionテーブルはPHPのセッションIDを主キー、値はSessionクラスの_param変数をserializeした値が格納される。
_param変数はユーザー情報の配列が格納されていて、ソースコード中では「userId」をキーとした値のみしか格納されない。

つまり、Sessionクラスで保持しているuserIdをyamatoにするような攻撃ができれば、現在のセッション情報を利用してうまい具合にノートが引き出せそう。

Yamlのライブラリを調査

怪しそうなyamlアップロード機能に着目して攻撃方法を考えた。
yamlのライブラリとしてpeclyamlライブラリが利用されていたので、そのドキュメントを熟読。
すると、yaml_parse関数のドキュメントの下部に以下のような注意書きを発見する。

警告
!php/object タグを使ったノードの unserialize() を有効にしている場合に、 ユーザーからの信頼できない入力を yaml_parse() で処理するのは危険です。 この挙動を無効にするには、ini 設定の yaml.decode_php を利用します。

!php/objectタグはyamlライブラリ独自のタグで、このタグが見つかった部分の要素を「PHPオブジェクトのserialize文字列」と認識し、その部分をunserializeしてオブジェクトに変換しその要素の値とする。

PHP Object Injection攻撃

このunserializeが勝手に行われるとなぜ危険なのかを調べると「PHP Object Injection」という攻撃方法があるからだとわかった。
PHP Object Injectionは「ユーザーからの入力を使ってunserializeを行ってインスタンスを作成した場合、そのインスタンスが生成される/破棄するときに実行される特殊関数__wakeup/__destructなどに動作に関わる重要な処理が記載されていた時に任意の攻撃が可能となる」という攻撃方法。

つまり「yaml内にPHPオブジェクトを生成する記述」があると、yamlライブラリの内部でunserializeされるのでPHP Object Injection攻撃が成功する可能性がある。
__wakeupとか__destructとかそんな気前よくあったかと探してみるとSessionクラスに__destruct、Dbクラスに__wakeupをそれぞれ発見。いよいよ怪しい。

SessionクラスとDbクラス

まずSessionクラスの__destructでは「データベースへセッション情報の書き込み」が行なわれている。

ということは、以下の流れで目的のNoteが取得できそう。

  • yamlの要素にSessionクラスのserialize文字列を指定、yaml_parse関数実行時に文字列がunserializeされSessionクラスのインスタンスが生成される
  • 動作が終わったと同時にインスタンスのデストラクタが走る
  • 事前に_param変数を調整しyamatoユーザーの情報を指定しておいて、その情報でSessionテーブルに書き込む

加えてセッション情報の書き込みには、Sessionクラスのdb変数にDbクラスのインスタンスも必要なので、これもserializeしておく。
Dbクラスの__wakeupには「データベースへの接続処理」があるので、unserializeされた時点でデータベースに接続済みの有効なインスタンスが生成される。これを忘れるとセッション情報の書き込みがうまくいかないので注意。

そんなこんなで以下のようなコードを書いてserialize文字列を生成した。セッションIDはクッキーから抜き出しておく。

<?php
class Db {};
class Session {
  private $_param = array("userId" => "yamato");
  public $id = "eq8l7n13m51pj85934r59nbqg2";
  public $db = null;
};

$session = new Session;
$session.db = new Db;
$param = array('session' => $session);
print yaml_emit($param);
session: !php/object "O:7:\"Session\":3:{s:15:\"\0Session\0_param\";a:1:{s:6:\"userId\";s:6:\"yamato\";}s:2:\"id\";s:26:\"eq8l7n13m51pj85934r59nbqg2\";s:2:\"db\";O:2:\"Db\":0:{}}"
最後の一声

生成したserialize文字列を利用しyamlをアップロードしたが、なぜかserialize文字列がそのままNoteの内容として保存されてしまってうまくいかない。

ソースコードをさらに追うと、yaml文字列の入力処理で「null文字を除去」している部分を発見。確かにserialize文字列にはnull文字が含まれていた。
そのためunserializeに失敗し、null文字が除去された状態のserialize文字列が保存されてしまっていた。

ただ、null文字の除去に利用されている正規表現が「Unicodeのnull文字(\u0000)」までは対応していなかったため、serialize文字列のnull文字をUnicode形式に置換。
置換後もう一度アップロードすると、null文字は除去されずにunserializeが成功したようで無事セッション情報が書き替わりyamatoユーザーのNoteが表示されてflagをゲット。


最後に

おそらく自分で筋道立てて正解できたのは初めてだったので、flagが出力されたときは結構うれしかったですw
他の問題はほとんど手をつけられなかったのが心残りですが、常設型のCTFサイトとかで練習しながらCTF力を上げていきたいと思います。