Notes

[harekazectf2019]easyNotes

题目来源:BUUCTF
题目类型:WEB
设计考点:Session伪造,Session反序列化

web1

web2
这是一个note服务网页,登录后可以添加note包括标题和内容,然后可以打包成ZIP或者TAR文件进行下载,题目提供了源码首先进行代码审计

1
2
3
4
5
6
7
8
9
10
11
12
<section>
<h2>Get flag</h2>
<p>
<?php
if (is_admin()) {
echo "Congratulations! The flag is: <code>" . getenv('FLAG') . "</code>";
} else {
echo "You are not an admin :(";
}
?>
</p>
</section>

通过审计该代码,我们发现当is_admin函数返回值为true时即可得到flag,我们跟进is_admin函数

1
2
3
4
5
6
7
8
function is_admin() {
if (!isset($_SESSION['admin'])) {
return false;
}
return $_SESSION['admin'] === true;
}
发现他是获取的SESSION文件中的admin值
那么思路就是怎么伪造session
1
2
3
#config.php
<?php
define('TEMP_DIR', '/var/www/tmp');

这个文件标识了临时文件存储路径

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
#lip.php(定义了一些函数)
<?php
function redirect($path) {
header('Location: ' . $path);
exit();
}

// utility functions
function e($str) {
return htmlspecialchars($str, ENT_QUOTES);
}

// user-related functions
function validate_user($user) {
if (!is_string($user)) {
return false;
}

return preg_match('/\A[0-9A-Z_-]{4,64}\z/i', $user);
}

function is_logged_in() {
return isset($_SESSION['user']) && !empty($_SESSION['user']);
}

function set_user($user) {
$_SESSION['user'] = $user;
}

function get_user() {
return $_SESSION['user'];
}

function is_admin() {
if (!isset($_SESSION['admin'])) {
return false;
}
return $_SESSION['admin'] === true;
}

// note-related functions
function get_notes() {
if (!isset($_SESSION['notes'])) {
$_SESSION['notes'] = [];
}
return $_SESSION['notes'];
}

function add_note($title, $body) {
$notes = get_notes();
array_push($notes, [
'title' => $title,
'body' => $body,
'id' => hash('sha256', microtime())
]);
$_SESSION['notes'] = $notes;
}

function find_note($notes, $id) {
for ($index = 0; $index < count($notes); $index++) {
if ($notes[$index]['id'] === $id) {
return $index;
}
}
return FALSE;
}

function delete_note($id) {
$notes = get_notes();
$index = find_note($notes, $id);
if ($index !== FALSE) {
array_splice($notes, $index, 1);
}
$_SESSION['notes'] = $notes;
}
#init.php
<?php
error_reporting(0);

require_once('config.php');
require_once('lib.php');

session_save_path(TEMP_DIR);#标识了session文件保存路径
session_start();
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
#export.php
<?php
require_once('init.php');

if (!is_logged_in()) {
redirect('/?page=home');
}

$notes = get_notes();

if (!isset($_GET['type']) || empty($_GET['type'])) {
$type = 'zip';
} else {
$type = $_GET['type'];
}

$filename = get_user() . '-' . bin2hex(random_bytes(8)) . '.' . $type;
$filename = str_replace('..', '', $filename); // avoid path traversal
$path = TEMP_DIR . '/' . $filename;

if ($type === 'tar') {
$archive = new PharData($path);
$archive->startBuffering();
} else {
// use zip as default
$archive = new ZipArchive();
$archive->open($path, ZIPARCHIVE::CREATE | ZipArchive::OVERWRITE);
}

for ($index = 0; $index < count($notes); $index++) {
$note = $notes[$index];
$title = $note['title'];
$title = preg_replace('/[^!-~]/', '-', $title);
$title = preg_replace('#[/\\?*.]#', '-', $title); // delete suspicious characters
$archive->addFromString("{$index}_{$title}.json", json_encode($note));
}

if ($type === 'tar') {
$archive->stopBuffering();
} else {
$archive->close();
}

header('Content-Disposition: attachment; filename="' . $filename . '";');
header('Content-Length: ' . filesize($path));
header('Content-Type: application/zip');
readfile($path);

可以发现session文件保存路径与note文件导出的路径一样,看看文件名的命名方式

1
2
3
$filename = get_user() . '-' . bin2hex(random_bytes(8)) . '.' . $type;
$filename = str_replace('..', '', $filename); // avoid path traversal
$path = TEMP_DIR . '/' . $filename;

通过get_user函数获取用户名然后用一段随机字符串加上文件类型,这里的get_user内容可控,登录时修改用户名即可,但是他还有type拼接且type无法为空不然就会被设置为ZIP,第二行代码中将..替换为空为了防止目录穿越,那么我们就可以让type的值为.这样filename中就有..被替换为空那么后缀名就消失了,我们成功构造出了一个符合要求的session文件名,看看内容是否可控

1
2
3
4
5
6
7
for ($index = 0; $index < count($notes); $index++) {
$note = $notes[$index];
$title = $note['title'];
$title = preg_replace('/[^!-~]/', '-', $title);
$title = preg_replace('#[/\\?*.]#', '-', $title); // delete suspicious characters
$archive->addFromString("{$index}_{$title}.json", json_encode($note));
}

内容是可控的我们新建note的title值会被写入到文件当中,那么思路就结束了

1
2
3
4
5
1、登录用户名设置为sess_
2、新建note,title值为|N;admin|b:1; (body任意)
3、export文件,BP拦截将type值设置为.
4、将文件名中SESS_后的字符串记录
5、打开flag获取页面设置session值为第四步的字符串,这样他就会自动反序列化/var/www/tmp中我们植入伪造admin的文件,这样$_session['admin']=true从而获取flag

Notes
https://lvyzcc.github.io/2025/03/15/[harekazectf2019]easyNotes/
作者
LvYz
发布于
2025年3月15日
许可协议