目录
第一章 PHP序列化基础
1.1 PHP序列化
1.1.1 PHP序列化概述
1.1.2 标准序列化
1.1.3 自定义序列化
1.1.4 序列化存储和转发
1.2 PHP反序列化
1.2.1 标准反序列化
1.2.2 未定义类的反序列化
1.2.3 Protected、Private属性反序列化
1.3 PHP序列化相关magic函数
1.3.1 __construct()和__destruct()
1.3.2 __sleep()和__wakeup()
1.3.3 __toString()
第二章 PHP反序列化漏洞
2.1 PHP反序列化漏洞简介
2.2 PHP反序列化漏洞利用
2.2.1 __wakeup()绕过
2.2.2 结合PHP代码审计,寻找漏洞
2.3 PHP反序列化漏洞总结
申明:本文适合PHP反序列化漏洞初学者,从PHP序列化原理讲起,逐渐深入到反序列化及其漏洞,几乎每个知识点都配有代码演示,十分方便理解,希望对你们有所帮助!另外,本文只作为博主学习路上的记录罢了,起到督促作用,文内引用网上一些前辈的观点,如有雷同实属借鉴,嘻嘻
在学习序列化之前,我们有必要了解其产生背景,PHP文件在执行结束后,相关的对象就会被销毁,此时如果另有其他页面需要调用这些对象是无法实现的,因为我们不可能让PHP文件一直无休止的去执行以保证对象不被销毁,于是在这样的需求推动下,序列化技术就产生了,所以,所谓的序列化实际上是为了方便数据的存储和转发。在PHP中序列化和反序列化一般用作缓存,比如session缓存、cookie缓存等,但是需要注意,PHP对Session的处理有三种引擎:
1、php_serialize ///序列化字符串形式与serialize()函数处理结果一致
2、php ///序列化字符串为"key|value"的形式
3、php_binary
每一种处理引擎得到的序列化字符串格式不一样,默认为php,可通过ini_set('session.serialize_handler','php_serialize')来指定引擎。
PHP序列化是PHP程序设计中的一种格式化数据的方法,通过序列化可以将对象(class)、数组(array)等进行序列化转换为特定格式的字符串,同时不丢失其类型和结构,以便于存储和传递,PHP实现序列化的函数是serialize(),来看个例子:
由上面的例子可见,序列化对不同类型的数据序列化后标识符是不同的,通过序列化字符串的首位进行标识,具体如下:
类型 | 标识 |
字符串 | s:size:value |
整型 | i:value |
布尔 | b:value |
NULL | N |
数组 | a:size:{key;value} |
对象 | O:Obj_name_len:Obj_name:var_num:{var_name_type:var_name_len:var_name;var_value_type:var_value_len:var_value} /// 注释:Obj-Object对象;var-varible变量 |
补充说明:
- 在序列化对象时,只会保留父类中的变量和自己申明的变量,而不会保留常量和方法(function)
- PHP通过关键字class来申明一个对象/类,在对象/类中使用$符申明变量/属性。
在序列化对象的时候,对于一些敏感的属性并不需要保存,比如密码,此时可自定义序列化。在调用serialize()函数进行序列化对象时,该函数首先会检查对象中是否存在一个magic函数__sleep(),如果存在则先调用__sleep(),然后才执行序列化操作,因此可以通过重载__sleep()函数来实现自定义序列化行为。
一般而言可使用函数file_put_contents()来实现序列化字符串的存储,序列化的转发可通过转发存储在本地的文件来实现,如下面例子所示:
通过上一节的讲解,我们知道数组、对象可以通过序列化为字符串进行存储,那么如何将序列化字符串还原为数组、对象呢?PHP提供了unserialize()反序列化函数来实现这一功能,如果反序列化的是对象,则在成功重构对象后,PHP会自动试图去调用magic函数__wakeup()。反序列化时,会尽量匹配预定义对象的变量名并赋值。
在上面的例子中,我们在调用反序列化函数之前,预先定义了person类,因此在执行反序列化之后,PHP会尽可能匹配反序列化后的变量名和预定义类person中的变量名,并进行赋值。如果我们在执行反序列化时,没有预先定义对象会发生什么呢?来看个例子:
比起之前预定义person类的结果,此处反序列化后构建的对象是__PHP_Incomplete_Class,并指明了未定义类的类名,此时我们尝试调用该对象看看会发生什么,如下所示:
显然此时报错了,提示incomplete object(不完整对象),并且从报错信息中可以看到规避方法:
1、在使用unserialize()之前定义对象;
2、使用__autoload()函数,指定加载对象的定义文件,如下图所示:
__autoload()接收的参数就是欲加载的类的名称,new person()时,由于不存在person对象,因此该对象名person会作为参数传递给__autoload(),在__autoload()中通过require_once方法来加载person.php文件,而正是person.php中定义了person对象,person.php文件内容如下:
1、protected、private类型属性序列化
2、protected、private类型属性反序列化
首先,看看序列化字符串不经过任何修改而反序列化会发生什么?
此时,很明显报错了,这是因为protected、private类型属性的序列化字符串中存在不可见字符%00(%00实际代表空字符,会被浏览器渲染时过滤掉,因此上面例子中并未显现出来),因此,我们在进行反序列化时需要手动将%00添加进去,如下所示:
PHP将所有__(两个下划线)开头的方法保留为magic函数,在PHP序列化操作中,有一些预定义的magic函数会被调用,比如__construct()、__destruct()、__toString()、__wakeup()、__sleep()等
在上面的例子中,似乎我们在new一个对象后,便可直接引用变量,实际上发生了一些我们看不到的事情,看看下面的例子,通过这个例子我们便能比较直观的理解magic函数__construct()和__destruct()的效果了
接下来我们理解一下__wakeup()和__sleep()函数,其中__sleep()在一个对象被序列化的时候调用,而__wakeup()在一个对象被反序列化的时候被调用,来看看下面的例子,帮助我们理解这两个函数:
最后,我们通过下面这个例子来理解__toString(),当对象被当作字符串的时候会自动调用__toString(),如下:
这里特别说一下__toString()魔术函数,触发它被调用的条件是比较多的,常见的触发条件有:
- echo/print $obj
- 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行==比较时
- 反序列化对象参与格式化SQL语句并绑定参数时
- 反序列化对象作为PHP字符串函数的参数时,比如strlen()、addslashes()等
- 反序列化对象作为class_exists()的参数时
根据前面的分析,利用PHP反序列化漏洞的突破口在unserialize(),只要我们能够控制unserialize()的参数,那我们便能传入任何序列化字符串。但是,反序列化字符串中只有对象的属性,而无方法,因此我们没法直接控制代码的执行,那该怎么办呢?通过magic函数来操控反序列化过程,下面我们通过例子来帮助理解
1、构造漏洞代码
target = "it is __wakeup()!";
}
function __destruct(){
$fp = fopen("F:/00 ProgramFiles/PhpStudy2018/PHPTutorial/WWW/wakeup.php","w");
fputs($fp,$this->target);
fclose($fp);
}
}
$cmd = $_GET['cmd'];
$cmd_unserialize = unserialize($cmd);
include("wakeup.php");
?>
2、漏洞代码分析
在执行unserialize()函数前会先检查是否存在__wakeup(),因此首先执行__wakeup(),为变量$target赋值"it is __wakeup()!",最后程序执行结束,对象被销毁,__destruct()被调用,将$target的值写入wakeup.php。因此,该代码正常情况下,无论GET传递什么参数,wakeup.php之中始终写入的都是"it is __wakeup()!"
3、漏洞代码利用
构造恶意反序列化字符串,使得表示对象属性个数的值大于实际属性的个数,绕过__wakeup(),此时,GET传递的参数首先赋值给$target,然后随$target被写入wakeup.php,从而实现恶意代码执行
第一步:构造恶意序列化字符串:可先使用serialize()产生正常序列化字符串,再将其修改为恶意序列化字符串,注意由于此处$target变量未赋值,因此在序列化后,对应的属性值为N,需将N修改为"类型:长度:值"的形式,如下所示:
构造恶意序列化字符串为:O:9:"class_001":2:{s:6:"target";s:27:"";}
第二步:通过GET方法提交恶意序列化字符串
说明,由于恶意序列化字符串本身不符合语法规则,因此PHP在解析时会抛出Notice告警,但并不影响漏洞代码执行,可借助error_reporting()避免显示Notice告警,如下:
第三步:使用恶意上传的脚本wakeup.php来getshell(蚁剑、中国菜刀等)
1、构造漏洞代码
function_a();
}
public function function_a(){
$this->a->close();
}
}
class B{
public $exp;
function close()
{
eval($this->exp);
}
}
$cmd = $GET_[666];
$cmd_unserialize = unserialize($cmd);
?>
2、漏洞代码分析
1、首先,粗略看一眼代码,发现class B中有eval()函数,eval()往往容易造成PHP远程代码执行,但是,漏洞代码中eval()的参数$exp并不可控,因此无法直接利用eval()执行远程代码。
2、接下来,我们的思路便是:如何让eval()的参数$exp变得可控,从而传入恶意代码
3、分析发现,class A里面的function_a()方法调用了一个未知的close()函数,那是否有办法让它去调用B里面的close()呢?
4、答案是肯定的,只需要满足下面两个条件:
1) A的属性$a是B的对象实例:$a = new B()
2) B的属性$exp可操控5、最后,让d)中的两个条件成为现实,PHP序列化代码如下图,O:1:"A":1:{s:1:"a";O:1:"B":1:{s:3:"exp";s:10:"phpinfo();";};}
3、GET提交恶意序列化字符串,实现代码注入
利用GET方法提交构造的恶意序列化脚本,如下所示,可以发现我们构造的恶意代码(phpinfo())被执行了
我们来分析一下反序列化执行的过程:
1、GET提交恶意序列化字符串后,首先会构建一个对象A的实例(即new A()),脚本执行结束后,A会依次调用__destruct()和function_a()
2、由于恶意序列化字符串中,A的属性$a值为对象B的实例、其属性$exp的值为"phpinfo();",因此在function_a()中调用close()方法时会将"phoinfo();"作为参数传递给eval()
3、此时"phoinfo();"会被直接作为PHP代码解析
经过前面的学习,想必对PHP反序列化漏洞已经有了初步的理解,反序列化漏洞危险而普遍,在CTF中往往会和PHP代码审计结合命题,即根据已知的PHP代码片段,分析存在的PHP序列化漏洞,然后构造恶意序列化字符串,最终实现攻击目的。此外,更为重要的是想要利用PHP反序列化漏洞,一定要对__construct()、__destruct()、__wakeup()、__sleep()、__toString()这些magic函数有足够的认识,这样才能更具它们的特性来构造恶意序列化字符串。
祝愿小伙伴们在成长的路上乘风直上!!!