-
-
[原创]Netgear Orbi RCE分析
-
发表于: 2025-9-17 03:08 443
-
## 概述
- 漏洞信息:在Netgear Orbi web服务中,存在root权限的未授权代码执行。
- 前提条件:无
- 漏洞简介: Netgear Orbi路由器中存在多个漏洞,未经身份验证的远程攻击者可以利用此漏洞链在易受攻击的服务器上执行任意代码。
- 影响型号及版本:RBR X0等 <= 次新版本
如 RBR50 (Orbi Router) Firmware Version 2.7.5.4
e69K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2F1k6i4c8Y4k6h3q4J5i4K6u0W2j5$3!0E0i4K6u0r3M7%4g2H3M7r3!0J5N6q4)9J5c8Y4m8J5L8$3c8#2j5%4c8Q4x3V1k6J5j5Y4t1#2x3q4)9J5c8W2)9K6c8W2)9#2k6X3N6S2i4K6y4p5x3W2)9J5k6e0p5J5z5e0M7@1x3K6f1$3y4q4)9J5k6e0p5&6x3e0p5@1x3K6x3I4z5e0y4Q4x3X3f1I4y4K6t1$3y4U0b7%4y4U0p5$3i4K6u0V1x3e0l9$3x3U0M7#2y4K6x3%4x3q4)9J5k6e0p5%4x3U0j5$3y4o6M7$3x3e0j5`.
- 漏洞不影响 分机 RBS
## 漏洞分析
### 漏洞1 身份认证绕过
在web组件的net-cgi程序中,存在`recover.cgi`的路由:
```
dispatch_entry_t <aRecoverCgi, aTextHtml, aCacheControlNo, \ ; "recover.cgi" "text/html" "Cache-Control: no-cache,no-store\r\nPra"...
.data:000D87C8 post_recover, sub_D894, 0>
```
```C
FILE *__fastcall post_recover(int a1, FILE *stream, int a3)
{
//...
v3 = a3 + 1;
v4 = (unsigned int)(a3 + 1) >= 0x7FFF;
v6 = byte_DE8CC;
if ( v4 )
v9 = 0x7FFF;
else
v9 = v3;
if ( j_fgets(byte_DE8CC, v9, stream) )
{
//...
LODWORD(v28[v13]) = 0;
v23 = time(0);
while ( 1 )
{
if ( !v12 )
return process_post_data((int)v28, v13);
//...
if ( v27 == -1 )
return process_post_data((int)v28, v13);
LABEL_33:
--v12;
}
}
result = console_log("Error: post_apply, not get post_buf.\n");
ts_flag = 1;
return result;
}
```
```C
int *__fastcall process_post_data(int a1, int a2)
{
//...
v4 = (int *)&algn_1156A1[27];
do
{
v4[1] = 0;
++v4;
}
while ( v4 != &dword_115878 );
v5 = (const char *)cgi_get((int)"submit_flag", a1, a2);
v6 = v5;
if ( v5 )
{
result = (int *)sub_147C0(v5);
if ( result )
{
ts_flag = 1;
}
else
{
//...
}
else
{
result = (int *)console_log("Error: covery_setobject, not found submit_flag.\n");
ts_flag = 1;
}
return result;
}
```
该handler接收传递过来的`post buf`,**处理 `submit_flag`**:
- 如果 `submit_flag` 存在,调用 `sub_147C0` 函数进行处理。
```C
int __fastcall sub_147C0(const char *a1)
{
//...
v2 = config_get((int)"http_loginname");
sub_7B630((int)v6, v2);
v3 = config_get((int)"http_guestname");
sub_7B630((int)v7, v3);
if ( cmp_str(v6, v7) )
{
if ( !timestamp[0] )
goto LABEL_3;
if ( !config_match(a1, timestamp) )
{
if ( timestamp[0] )
{
v5 = (const char *)config_get((int)a1);
console_log("ERROR: timestamp is not matched. t1:%s t2:%s\n", v5, timestamp);
goto LABEL_4;
}
LABEL_3:
console_log("ERROR: timestamp for:%s is NULL", a1);
LABEL_4:
result = 1;
ts_flag = 1;
return result;
}
if ( cmp_str(a1, "backup_save")
&& cmp_str(a1, "upgrade_check")
&& cmp_str(a1, "guest_portal_upload")
&& cmp_str(a1, "sso_status") )
{
if ( cmp_str(a1, "upgrade_check_lte") )
config_unset((int)a1);
}
}
return 0;
}
```
```C
bool __fastcall config_match(char *a1, const char *a2)
{
const char *v3; // r0
v3 = (const char *)j_config_get(a1);
return strcmp(v3, a2) == 0;
}
```
`config_match` 根据传入的键名(a1)从配置系统中获取对应的值,将获取到的配置值与期望的字符串(a2)进行比较,返回比较结果。
在`handle_http_request`中,可以看到对`timestamp`的处理,主要是有URL的传递参数来进行赋值的,是可以控制的。 所以可以控制了以下的数据:
- timestamp
- submit_flag对应的action
若 `timestamp`控制为3,且submit_flag为`hijack_process`,则会进入到`config_unset((int)a1);`,进而似的`hijack_process`被unset,变为空,进而不等于3,造成身份认证绕过漏洞的存在。
### 漏洞2 任意文件创建及修改
在路由处理中,`upload.cgi`:
```C
dispatch_entry_t <aUploadCgi, aTextHtml, aCacheControlNo, \ ; "upload.cgi" "text/html" "Cache-Control: no-cache,no-store\r\nPra"...
.data:000D87E0 post_upload_apply, do_upload_apply, auth_check>
```
```C
int __fastcall post_upload_apply(int a1, FILE *a2, int a3)
{
sub_25FF4("uploadebug", "upload.cgi---------- %s %d\n", "post_upload_apply", 750);
sub_75D9C((int)"1", "/tmp/upload_flag");
if ( max_post_size >= a3 )
{
if ( !handle_upload(a2, a3) )
{
cgi_save_file();
return unlink("/tmp/upload_flag");
}
}
else
{
//...
}
ts_flag = 1;
return unlink("/tmp/upload_flag");
}
```
```C
int __fastcall handle_upload(FILE *stream, int a2)
{
//...
dirp = opendir("/tmp/uploads");
if ( dirp )
closedir(dirp);
else
mkdir("/tmp/uploads", 0x1F6u);
v4 = getenv("CONTENT_TYPE");
if ( v4 )
{
v5 = n_strstr(v4, " boundary=");
//...
*/ while ( 1 )
{
v15 = (_BYTE *)sub_39370(s_4);
v16 = (int)v15;
if ( !v15 )
goto LABEL_22;
c = (unsigned __int8)*v15;
if ( !*v15 )
break;
v14 = sub_7B66C("Content-Disposition: form-data;");
if ( !sub_7B654(v16, "Content-Disposition: form-data;", v14) )
{
s_6 = (char *)sub_39304(v16, " filename=");
v30 = (const char *)sub_39304(v16, " name=");
if ( !v30 )
{
v16 = 0;
sub_25F6C("can not find 'name' field in 'Content-Disposition:'\n");
LABEL_22:
if ( a2 <= 0 && v39 )
goto LABEL_46;
LABEL_75:
sub_31B20(a2, stream);
if ( !v16 )
goto LABEL_46;
return 0;
}
sub_7B5A0(s_3, 0x100u, "%s", v30);
v40 = 0;
if ( s_6 )
{
if ( *s_6 )
{
v31 = strrchr(s_6, '\\'); // 代码只处理了 Windows 风格的路径分隔符 \,没有处理 Linux 风格的 /
if ( v31 )
s_6 = v31 + 1;
}
v40 = 1;
filename = (const char *)constructUploadPath(s_3, s_6, 1);
}
}
}
if ( v40 != 1 )
{
streama = (FILE *)(unsigned __int8)*v15;
memset(&s__0, c, 0x400u);
c = c;
goto LABEL_29;
}
if ( filename )
{
if ( !*filename )
{
streama = (FILE *)*(unsigned __int8 *)filename;
goto LABEL_29;
}
streama_1 = fopen(filename, "wb");// 写入文件
streama = streama_1;
if ( streama_1 )
goto LABEL_29;
v35 = _errno_location();
v36 = strerror(*v35);
sub_25F6C("cannot create file %s: %s\n", filename, v36);
sub_31B20(a2, stream);
return -1;
}
```
```C
void *__fastcall constructUploadPath(const char *form_name, const char *filename, int path_flag)
{
memset(s, 0, sizeof(s));
if ( path_flag != 1 || !*filename )
{
sub_7B5A0(s, 0x420u, "%s", filename);
goto LABEL_3;
}
sub_7B5A0(s, 0x40Du, "%s/%s", "/tmp/uploads", filename);
if ( access(s, 0) )
{
LABEL_3:
v6 = dword_F1D20;
if ( dword_F1D20 )
{
n49 = 1;
while ( n_strcmp(form_name, *(const char **)v6) )
{
/*
... omit code
*/
}
ptr = *(void **)(v6 + 4);
if ( ptr )
{
free(ptr);
ptr = (void *)sub_7B81C(s);
*(_DWORD *)(v6 + 4) = ptr;
}
}
else
{
LABEL_28:
v13 = malloc(0xCu);
*v13 = sub_7B81C(form_name);
ptr = (void *)sub_7B81C(s);
v13[1] = ptr;
v13[2] = 0;
if ( dword_F1D20 )
*(_DWORD *)(v6 + 8) = v13;
else
dword_F1D20 = (int)v13;
}
return ptr;
}
n50 = 1;
memset(s_1, 0, sizeof(s_1));
v10 = strrchr(s, '.'); //查找最后一个.字符,用于获取文件扩展名。
if ( v10 )
v11 = v10 + 1;
else
v11 = 0;
if ( v10 )
*v10 = 0;
do
{
if ( v11 )
sub_7B5A0(s_1, 0x420u, "%s_%d.%s", s, n50, v11);
else
sub_7B5A0(s_1, 0x420u, "%s_%d", s, n50);
if ( access(s_1, 0) )
{
memcpy(s, s_1, sizeof(s));
goto LABEL_3;
}
++n50;
}
while ( n50 != 50 );
sub_25F6C("exist same filename in %s, rename times reach to 50, ignore it!\n", v12);
return 0;
}
```
在文件上传的功能中,没有对 filename 进行安全性验证(如`..`、`/`),存在路径穿越漏洞。
### 漏洞3 命令注入
```C
int __fastcall tag_http_header(FILE *s_1, char **a2)
{
/*
... omit code
*/
WW_1 = cat_file("/tmp/firmware_region");
if ( *WW_1 )
WW = WW_1;
else
WW = "WW";
/*
... omit code
*/
v14 = sub_7B3F0("nc_google_analytics");
if ( v14 )
{
v14 = strncmp(WW, "PR", 2u);
if ( v14 )
{
memset(s, 0, sizeof(s));
if ( config_match((int)"ga_usr", (int)"") )
{
time(&timer);
v17 = (const char *)sub_2BCAC();
sub_7B5A0(v22, 0x20u, "%s%ld", v17, timer);
snprintf(s_2, 0x40u, "echo -n %s | md5sum", v22);
stream = (FILE *)cmd_run(s_2, "r");
stream_1 = stream;
if ( stream )
{
j_fgets(s, 64, stream);
s[32] = 0;
pclose(stream_1);
}
config_set("ga_usr", s);
}
else
{
v20 = (const char *)config_get("ga_usr");
sub_7B5A0(s, 0x40u, "%s", v20);
}
/*
... omit code
*/
}
```
如果在一个 HTML 文件中插入 `<% http_header("", "") %>` 这样的模板代码,并通过服务器访问该 HTML 文件,服务器会解析这个模板语法,并最终调用 `.data` 节中的 `tag_http_header` 函数来处理这个请求。
```C
int sub_2BCAC()
{
return sub_75F68("artmtd -r sn", "/tmp/Seria_Number");
}
FILE *__fastcall cmd_run(const char *command, const char *modes)
{
bool v2; // zf
v2 = modes == 0;
if ( modes )
v2 = command == 0;
if ( v2 )
return 0;
else
return popen(command, modes);
}
```
进而会触发到`sub_2BCAC`函数来返回 `/tmp/Seria_Number`的文件内容,然后进行命令拼接,进入到`cmd_run`函数当中,其中 `v22` 的内容未经过严格的校验或过滤且该文件可利用可控,导致命令注入。
(先前的这2个条件都可以利用之前的漏洞进行创造,可以参考EXP部分)
## 总结
先使用漏洞1绕过身份认证漏洞,触发到`upload.cgi`,接着使用路径遍历的漏洞,创建出任意想要的文件,接着访问特制的html文件,触发命令注入漏洞。
使用整个漏洞利用链,使得Netgear Orbi的部分系列设备中,存在root权限的未授权代码执行漏洞。
