Tracing command injection across router interfaces.

Research notes on CVE-2022-28571, CVE-2022-28572, and CVE-2022-28573.

On this page

Research notes · CVE-2022-28571–28573 research notes

Introduction

These notes document several command injection vulnerabilities I found during the same period. CVE-2022-28571 && CVE-2022-28572 CVE-2022-28571 stems from incomplete filtering of Telnet CLI parameters. It requires a known password, so its impact is constrained by authentication. The discovery process was interesting, and this article focuses on that vulnerability. CVE-2022-28572 is a command injection vulnerability in the Tenda AX18 series. I identified two other potential triggers, but submitted the one that was easiest to reproduce. CVE-2022-28573 is a straightforward command injection vulnerability in the D-Link 823 Pro.

Analyzing CVE-2022-28571

I started by reproducing vulnerabilities in the D-Link DIR-882, including CVE-2021-45998. Packet captures suggested strict request validation: each AUTH value appeared to work only once. The vulnerability reproduced, but my usual command for starting a shell did not work:

SHELL
telnetd -l /bin/sh -p 2333
The shell did not connect. I wondered whether the DIR-882 used an unusual Telnet implementation, so I searched the firmware for Telnet-related code.
Telnet-related strings appeared in lighttpd, suggesting that an HTTP request could start Telnetd. I followed the string references in lighttpd.
This revealed the /start_telnet route and what appeared to be its launch command, telnetd -b 0.0.0.0.
On the physical device I had bought for about CNY 200, a GET request started the service without authentication. The response was a 404, but the authenticated system-debug page returned a file containing system information. It confirmed that the command had run, and port 23 was now open.
At this point I shifted from reproducing an existing issue to investigating the Telnet service. My initial hypothesis was that its password might be hardcoded. That hypothesis later proved incorrect. The next step was to locate the password. Connecting to Telnet produced the following prompt:
The prompt included dlinkrouter login. Based on earlier Tenda research, I searched for references to that string, but found none. That suggested dlinkrouter was generated from system information. Telnetd was linked into BusyBox, so I moved the analysis there.
Cross-references to the login string led to the following code:
sub_465960 retrieves uname, suggesting that dlinkrouter comes from the system name. Following the remaining references exposed the Telnet login call path.
C
 login();                                    // readusername
    do
    {
      if ( *(_DWORD *)(_stdin + 72) )
      {
        v11 = *(unsigned __int8 **)(_stdin + 16);
        if ( (unsigned int)v11 < *(_DWORD *)(_stdin + 24) )
        {
          v12 = *v11;
          *(_DWORD *)(_stdin + 16) = v11 + 1;
          goto LABEL_27;
        }
        v13 = (int (*)(void))&_fgetc_unlocked;
      }
      else
      {
        v13 = (int (*)(void))&fgetc;
      }
      v12 = v13();
      if ( v12 == -1 )
        goto LABEL_67;
LABEL_27:
      if ( v12 == 10 )
      {
        if ( !--v10 )
          goto LABEL_67;
        goto LABEL_20;
      }
    }
    while ( isspace(v12) );
    username[0] = v12;
    if ( !fgets(&username[1], 30, stdin) || (v14 = &username[1], !strchr(&username[1], 10)) )
LABEL_67:
      exit(1);
    while ( isgraph((unsigned __int8)*v14) )
      ++v14;
    *v14 = 0;
LABEL_37:
    user_struct = getpwnam(username);           // Retrieve the account record from the password database.
                                                // #include <sys/types.h>
                                                // #include <pwd.h>
                                                // struct passwd
                                                // {
                                                //   char *pw_name;                /* Login name */
                                                //   char *pw_passwd;              /* Password hash */
                                                //   __uid_t pw_uid;               /* User ID */
                                                //   __gid_t pw_gid;               /* Group ID */
                                                //   char *pw_gecos;               /* User information */
                                                //   char *pw_dir;                 /* Home directory */
                                                //   char *pw_shell;               /* Login shell */
                                                // };
                                                // 
    pw_name = user_struct;
    if ( !user_struct )
    {
      strcpy(username, "UNKNOWN");
      goto LABEL_49;
    }
    v17 = **(unsigned __int8 **)(user_struct + 4);
    if ( v17 != 0x21 && v17 != 0x2A )
    {
      if ( (v5 & 1) != 0 )
        goto LABEL_53;
      if ( *(_DWORD *)(pw_name + 8) )
        goto LABEL_82;
      v26 = "/etc/securetty";
      v18 = (_DWORD *)sub_463D20("/etc/securetty", sub_4063C8);
      while ( sub_463DE4(v18, &v26, 459009, "# \t") && strcmp(v26, dword_485294) )
        v26 = 0;
      sub_463D74(v18);
      if ( v26 )
      {
LABEL_82:
        if ( **(_BYTE **)(pw_name + 4) )        // pw_passwd
        {
LABEL_49:
          if ( !sub_45D554(pw_name) )
            goto LABEL_50;
        }
LABEL_53:
        alarm(0);
        if ( v4 || access("/etc/nologin", 0) )
        {
          fchown(0, *(_DWORD *)(pw_name + 8), *(_DWORD *)(pw_name + 12));
          fchmod(0, 384);
          sub_45C9AC(pw_name);
          v22 = *(const char **)(pw_name + 24);
          if ( !v22 || !*v22 )
            v22 = "/bin/sh";
          sub_465A64(v22, (v5 & 4) == 0, 1, pw_name);
          v23 = open("/etc/motd", 0);
          if ( v23 >= 0 )
          {
            fflush(stdout);
            sub_407824(v23, 1);
            close(v23);
          }
          if ( !*(_DWORD *)(pw_name + 8) )
            syslog(6, "root login%s", v8);
          signal(14, 0);
          signal(2, 0);
          if ( v3 )
            v24 = *(const char **)(pw_name + 24);
          else
            v24 = "/usr/bin/cli";
          sub_465810(v24, 1, 0, 0);
        }
        v19 = (_DWORD *)sub_4063C8((int)"/etc/nologin");
        if ( !v19 )
        {
          puts("\r\nSystem closed for routine maintenance\r");
          goto LABEL_67;
        }
        while ( 2 )
        {
          if ( v19[18] )
          {
            v21 = (unsigned __int8 *)v19[4];
            if ( (unsigned int)v21 < v19[6] )
            {
              v20 = *v21;
              v19[4] = v21 + 1;
LABEL_57:
              if ( v20 == 10 )
                LOBYTE(v20) = 13;
              sub_407048(v20);
              continue;
            }
            v20 = ((int (__fastcall *)(_DWORD *))_fgetc_unlocked)(v19);
          }
          else
          {
            v20 = ((int (__fastcall *)(_DWORD *))fgetc)(v19);
          }
          break;
        }
        if ( v20 == -1 )
        {
          fflush(stdout);
          fclose(v19);
          goto LABEL_67;
        }
        goto LABEL_57;
      }
    }
C
BOOL __fastcall sub_45D554(int a1)
{
  const char *pw_passwd; // $s2
  BOOL v2; // $s0
  int v3; // $s1
  int v4; // $s3
  int v5; // $v0

  if ( a1 )
  {
    pw_passwd = *(const char **)(a1 + 4);
    v2 = 1;
    if ( !*pw_passwd )
      return v2;
  }
  else
  {
    pw_passwd = "aa";
  }
  v3 = sub_468334(0, (int)"Password: ");
  v2 = 0;
  if ( v3 )
  {
    v4 = sub_46521C();                          // v4 = enc(password,slat)
                                                // slat= "aa" || user->uid
    v2 = strcmp(v4, pw_passwd) == 0;
    free(v4);
    v5 = strlen(v3);
    memset(v3, 0, v5);
  }
  return v2;
}

Analysis of the key functions established the DIR-882 Telnet login flow:

  1. Enter a username.
  2. Look up the corresponding password record in /etc/passwd.
  3. Compare the salted password hash. I then investigated how that password record was generated. Reversing the main program, prog.cgi, showed the following: The password used for the /etc/passwd entry is the web login password with @twsz2018 appended.
Appending that suffix to the login password allowed access to Telnet. However, the session opened a CLI rather than a normal shell. Further analysis of BusyBox showed why:
C
          if ( !*(_DWORD *)(pw_name + 8) )
            syslog(6, "root login%s", v8);
          signal(14, 0);
          signal(2, 0);
          if ( v3 )
            v24 = *(const char **)(pw_name + 24);
          else
            v24 = "/usr/bin/cli";
          sub_465810(v24, 1, 0, 0);
        }

The configured program was /usr/bin/cli. I continued by reversing that CLI.

C
int __fastcall cmd_ping(int a1, const char *a2, _DWORD *a3, int a4)
{
  int v8; // $s0
  int v9; // $s1
  char v11[128]; // [sp+18h] [-80h] BYREF

  memset(v11, 0, sizeof(v11));
  v8 = snprintf(v11, 128, "%s ", a2);
  if ( a4 > 0 )
  {
    v9 = 0;
    do
    {
      ++v9;
      v8 += snprintf(&v11[v8], 128 - v8, "%s ", *a3++);
    }
    while ( v9 != a4 );
  }
  systemCmd(a1, (int)v11);
  return 0;
}
int __fastcall systemCmd(int a1, int a2)
{
  int v4; // $s2
  int v5; // $s0
  char v7[256]; // [sp+18h] [-100h] BYREF

  v4 = -1;
  memset(v7, 0, sizeof(v7));
  if ( a2 )
  {
    v5 = popen(a2, "r");
    if ( v5 )
    {
      while ( fgets(v7, 256, v5) )
        cli_bufprint(a1, "%s", v7);
      v4 = 0;
      cli_print(a1, "  ", v7);
      pclose(v5);
    }
  }
  return v4;
}
The input reaches popen directly. Appending a shell command therefore provides a command injection path.
I later found that the D-Link Telnetd command needed the following arguments to start a shell listener:
SHELL
telnetd -l /bin/sh -p 2333 -b 0.0.0.0

Disclosure timeline

  • March 8, 2022: Submitted the vulnerability for CVE assignment.
  • April 1, 2022: The submission was returned because version information was missing.
  • April 1, 2022: Resubmitted with the required information.
  • May 2, 2022: Assigned CVE-2022-28571.