php unserialize

php unserialize

之前没怎么接触过PHP反序列化的问题,这两天碰巧做了一道往年的题,遇到了,算是初步了解了PHP反序列化的问题,记录一下解决思路
题目链接:https://buuoj.cn/login?next=%2Fchallenges%3F#%5B0CTF%202016%5Dpiapiapia

环境探测

PHP版本5.6 没有发现其他有用信息
用扫描器扫了一下发现了网站备份,下载下来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<?php
class.php
require('config.php');

class user extends mysql{
private $table = 'users';

public function is_exists($username) {
$username = parent::filter($username);

$where = "username = '$username'";
return parent::select($this->table, $where);
}
public function register($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);

$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
return parent::insert($this->table, $key_list, $value_list);
}
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function show_profile($username) {
$username = parent::filter($username);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->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);
}
public function __tostring() {
return __class__;
}
}

class mysql {
private $link = null;

public function connect($config) {
$this->link = mysql_connect(
$config['hostname'],
$config['username'],
$config['password']
);
mysql_select_db($config['database']);
mysql_query("SET sql_mode='strict_all_tables'");

return $this->link;
}

public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}

public function insert($table, $key_list, $value_list) {
$key = implode(',', $key_list);
$value = '\'' . implode('\',\'', $value_list) . '\'';
$sql = "INSERT INTO $table ($key) VALUES ($value)";
return mysql_query($sql);
}

public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}

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);
}
public function __tostring() {
return __class__;
}
}
session_start();
$user = new user();
$user->connect($config);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
config.php
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
update.php
<?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 {
?>
<!DOCTYPE html>
<html>
<head>
<title>UPDATE</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;">
<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Please Update Your Profile</h3>
<label>Phone:</label>
<input type="text" name="phone" style="height:30px"class="span3"/>
<label>Email:</label>
<input type="text" name="email" style="height:30px"class="span3"/>
<label>Nickname:</label>
<input type="text" name="nickname" style="height:30px" class="span3">
<label for="file">Photo:</label>
<input type="file" name="photo" style="height:30px"class="span3"/>
<button type="submit" class="btn btn-primary">UPDATE</button>
</form>
</div>
</body>
</html>
<?php
}
?>

阅读源码后发现其资料更新流程是这样的:
在update.php点击更新资料后,在本页面处理,经过正则表达式过滤后通过的话,在user类中的update_profile方法中执行序列化后的profile
$user->update_profile($username, serialize($profile));
跟进user->update_profile方法:
用户名和资料经过过滤器fileter过滤后直接丢给mysql更新,继续跟进filter
主要是正则替换。把危险字段替换为“hacker”。这里有问题。暂时不管,继续跟进。
顺利通过filter后则可顺利更新用户资料。
再看profile.php里面这句话:
$photo = base64_encode(file_get_contents($profile[‘photo’]));
file_get_contents 是不是想到了什么?伪协议?
我们想读取到的文件是config.php 因为里面已经提示了有flag字段。
接下来我们要做的是让file_get_contents()包含config.php这个文件
先自己尝试下替换文件名,失败了。
接下来考虑伪协议,也失败了。。
然后考虑反序列化的问题
首先明白什么是反序列化
个人理解反序列化就是把对象,变量,各种属性不一的文件通过一种形式的转换转换成字符串(序列化),并且还能通过该字符串还原该对象(反序列化)
一个例子就是ss导出配置的时候给他base64解密看看是不是序列化的思想呢2333
举个栗子

反序列化严格按照长度进行反序列化,如果长度改变的话可能反序列化失败。比如:

在反序列化之前,我把最后一个字符串加了一个d 但是前面的长度还是5没有变,所以反序列化失败了,报错。
还有一种情况:
通过自己构造的反序列化对象,可以修改对象的某些属性。
比如之前c的最后一个字符串为234,我们构造$c1=’a:3:{i:0;s:3:”123”;i:1;s:3:”abc”;i:2;s:5:”wdnmd”;}”;i:2;s:3:”234”;}’;
就把最后一个属性的值换成了wdnmd,后面原来的字符串会被忽略。起到了夹带私货的作用。

这道题我们想让photo的值为config.php,所以需要利用上面说的字符串逃逸的问题,将payload挤到后面去。
具体什么意思?
对nickname的内容进行构造,我们知道,序列化之后长度固定不变。就是S:后面的数字。但是问题就出现在他存在一个字符串替换,把where替换成hacker,使得字符串长度+1。那么问题来了,如何利用?
首先绕过正则表达式对nickename的判断,用数组就好。nickname[]
我们想传入的字符串为 “;}s:5:”photo”;s:10:”config.php”;} 总长度为34,所以我们需要写34个where加上我们想传入的字符串作为payload,总长度为204(包括34个where和想传入的字符串)
在经过序列化,过滤的过程中,所有的where被替换成和hacker,这样序列化后的s:204不变,nickname的值变成了34个hacker,并且成功的修改了photo的值为config.php
payload:wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere”;}s:5:”photo”;s:10:”config.php”;}
注意:
任何不正确的反序列化是会失败的。
In the end:
php unserialize的问题有很多,我只是学习到了其中的一个,等学习到了其他的问题继续整理、巩固下来。